0

I would like to limit number of flattened structures inside an object. Consider example below.

Input:

const exampleObj = {
  foo: {
    bar: {
      biz: "hello"
    }
  }
};

Output:

{foo_bar_biz: "hello"}

I wanted to set a limiter (limit = 2) so that function will stop performing recursive function and return something like this

{foo_bar: "[Object] object"}

Here's a snippet:

const { useState, useEffect} = React;

const exampleObj = {
  foo: {
    bar: {
      biz: "hello"
    }
  }
};

let limit = 0;

function App() {
  const [state, setState] = useState({});

  useEffect(() => {
    flatten(exampleObj);
  }, []);

  const flatten = (data, parent, result = {}) => {
    for (let key in data) {
      const propName = parent ? parent + "_" + key : key;
      if (typeof data[key] === "object") {
        limit++;
        if (limit <= 1) {
          flatten(data[key], propName, result);
        } else {
          setState(prevState => {
            return {
              ...prevState,
              [propName]:
                typeof data[key] === "object" ? "[Object] object" : data[key]
            };
          });
        }
      } else {
        result[propName] = data[key];
        setState({
          ...state,
          [propName]: data[key]
        });
      }
    }
  };

  console.log(state);

  return (
    <div>
      <p>Start editing to see some magic happen :)</p>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>

<div id="root"></div>

I've prepared a solution for that, but it is quite cumbersome. https://stackblitz.com/edit/react-u5klvc

adiga
  • 34,372
  • 9
  • 61
  • 83
  • Please add the relevant code here as well. If the external link is deleted or modified, the question will have no future value. You can also create [a runnable stack snippet](https://meta.stackoverflow.com/questions/358992) – adiga Nov 18 '20 at 11:27
  • Or if a visitor cannot open the link for any other reason, like it's being blocked. – VLAZ Nov 18 '20 at 11:28
  • Ahh sorry, this link should work https://stackblitz.com/edit/react-td5w6j?file=src/App.js – Krzysztof Podmokły Nov 18 '20 at 11:39
  • The problem isn't the *link* - the policy on Stack Overflow is to have the entire information for the post ***in*** the post. Visitors shouldn't need to go off-site to get context for the question. – VLAZ Nov 18 '20 at 11:49
  • The link works just fine. I meant the link might not work in the future for some reason ([Link rot](https://en.wikipedia.org/wiki/Link_rot)) – adiga Nov 18 '20 at 11:49
  • Also, there are many solutions here: [Fastest way to flatten / un-flatten nested JSON objects](https://stackoverflow.com/questions/19098797) You just need to add the limit logic in there. – adiga Nov 18 '20 at 11:51
  • @adiga as a reminder, even *if* the link works, a visitor might still be unable to follow it because they might have it blocked on their network for whatever reason. – VLAZ Nov 18 '20 at 11:51
  • Okey, I get it. I will add full information regarding the problem next time. – Krzysztof Podmokły Nov 18 '20 at 11:53
  • 1
    How is react at all relevant here? This could be a simple question about data structures and changing them? – Yoshi Nov 18 '20 at 11:59
  • @Yoshi they are doing `setState` inside the recursive function. (Not sure why. It could be moved outside and set once the final output is returned) – adiga Nov 18 '20 at 12:02
  • That's what I mean. It would be a lot simpler to handle the restructuring and simple call `setState` on the final result. – Yoshi Nov 18 '20 at 12:04
  • 1
    Maybe this example will help you. It splits the *flattening* from the react part. If this is anywhere near of what you're trying to do, let me know. Maybe it warrants an answer. https://codesandbox.io/s/gracious-galois-gyc0v?file=/src/App.js – Yoshi Nov 18 '20 at 13:00
  • Thanks for the effort put in doing this example. This is almost what I needed thank you very much. The only part that was changed is `[`${outer}_${inner}`]: isObject(value) ? "[Data not flattened]" : value` – Krzysztof Podmokły Nov 18 '20 at 13:56

2 Answers2

2

Here's one possible implementation of flatten. Notice how it's completely decoupled from React or a specific React component. It works on any JavaScript objects -

const snakecase = s =>
  s.join("_")
  
const flatten = (t = {}, n = Infinity, join = snakecase) =>
{ const many = (t, n, path) =>
    n >= 0 && Object(t) === t
      ? Object.entries(t).flatMap(_ => one(_, n - 1, path))
      : [ [ join(path), t ] ]
      
  const one = ([ k, v ], n, path) =>
    many(v, n, [...path, k])
  
  return Object.fromEntries(many(t, n, []))
}

Given some example data -

const data =
  { a1: 11
  , a2: { b1: 21, b2: 22 }
  , a3: { b1: { c1: 311, c2: 312 }
        , b2: { c1: 321, c2: 322, c3: { d1: 3231 } }
        }
  }

A depth of 1 flattens one level -

flatten(data, 1)
{
  "a1": 11,
  "a2_b1": 21,
  "a2_b2": 22,
  "a3_b1": {
    "c1": 311,
    "c2": 312
  },
  "a3_b2": {
    "c1": 321,
    "c2": 322,
    "c3": {
      "d1": 3231
    }
  }
}

A depth of 2 flattens two levels -

flatten(data, 2) // => ...
{
  "a1": 11,
  "a2_b1": 21,
  "a2_b2": 22,
  "a3_b1_c1": 311,
  "a3_b1_c2": 312,
  "a3_b2_c1": 321,
  "a3_b2_c2": 322,
  "a3_b2_c3": {
    "d1": 3231
  }
}

The default depth is inifinity -

flatten(data) // => ...
{
  "a1": 11,
  "a2_b1": 21,
  "a2_b2": 22,
  "a3_b1_c1": 311,
  "a3_b1_c2": 312,
  "a3_b2_c1": 321,
  "a3_b2_c2": 322,
  "a3_b2_c3_d1": 3231
}

The default join is snakecase, but the parameter can be specified at the call site -

const camelCase = ([ first = "", ...rest ]) =>
  first + rest.map(upperFirst).join("")

const upperFirst = ([ first = "", ...rest ]) =>
  first.toUpperCase() + rest.join("")

flatten(data, 2, camelCase) // => ...
{
  "a1": 11,
  "a2B1": 21,
  "a2B2": 22,
  "a3B1C1": 311,
  "a3B1C2": 312,
  "a3B2C1": 321,
  "a3B2C2": 322,
  "a3B2C3": {
    "d1": 3231
  }
}

Expand the snippet below to verify the results in your own browser -

const data =
  { a1: 11
  , a2: { b1: 21, b2: 22 }
  , a3: { b1: { c1: 311, c2: 312 }
        , b2: { c1: 321, c2: 322, c3: { d1: 3231 } }
        }
  }
  
const snakecase = s =>
  s.join("_")
  
const flatten = (t = {}, n = Infinity, join = snakecase) =>
{ const many = (t, n, path) =>
    n >= 0 && Object(t) === t
      ? Object.entries(t).flatMap(_ => one(_, n - 1, path))
      : [ [ join(path), t ] ]
      
  const one = ([ k, v ], n, path) =>
    many(v, n, [...path, k])
  
  return Object.fromEntries(many(t, n, []))
}

const result =
  flatten(data, 2)

console.log(JSON.stringify(result, null, 2))
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    Haha I know the feeling. But it's not a competition Your solution of combining other generics is great demonstration of how functions can build upon each other. – Mulan Nov 18 '20 at 17:21
  • 1
    No, and I'm not really feeling competitive. We are attracted to the same sorts of questions, and have an interesting mix of answers which are very similar to one another's and those very different from one another's. It's quite fun, in fact. – Scott Sauyet Nov 18 '20 at 18:18
2

Thankyou's answer is great. But this is enough different to be worth posting, I think.

I keep handy functions like path and getPaths in my personal library. For this, I had to alter getPaths to accept a depth parameter (d) to escape earlier.

With these and Object.fromEntries we can easily write a partialFlatten function that does what I think you want:

const path = (ps = []) => (obj = {}) =>
  ps .reduce ((o, p) => (o || {}) [p], obj)

const getPaths = (obj, d = Infinity) =>
  Object (obj) === obj && d > 0
    ? Object .entries (obj) .flatMap (
        ([k, v]) => getPaths (v, d - 1) .map (p => [Array.isArray(obj) ? Number(k) : k, ...p])
      )
    : [[]]

const partialFlatten = (obj, depth) => 
  Object .fromEntries (
    getPaths (obj, depth) .map (p => [p .join ('_'), path (p) (obj)])
  )

const exampleObj = {foo: {bar: {biz: "hello"}, baz: 'goodbye'}}

console .log ('depth 1:', partialFlatten (exampleObj, 1))
console .log ('depth 2:', partialFlatten (exampleObj, 2))
console .log ('depth 3:', partialFlatten (exampleObj, 3))
.as-console-wrapper {max-height: 100% !important; top: 0}
Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103