question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Question: Subscribing to nested changes at a parent level

See original GitHub issue

Hi avkonst,

Amazing library you’ve built here. It looks exactly like the type of thing I was imagining. I’m going through evaluating libraries to try to get to a less hacky way of implementing state.

I had a question on a specific use case that’s not obvious to me if hookstate would handle well (or could have a plugin written to handle).

I have a large recursive nested structure, basically a workflow of questions and answers which lead to more questions then more answers and so on. I’ve built a hacked-together solution with regular react components (well, preact), where the top-level component has the entire tree as a state variable, passing down sections of the tree to its children, and then each nested component has an oninput that mutates its section of the tree directly and then calls forceUpdate(). This works because the tree of Components mirror the tree of the state, so forceUpdate() correctly cascades for the corresponding subtree. I’m sure many would cry at this implementation, but it works well enough.

In hookstate, it looks like nested combined with the usage of scoped states would work well enough as a replacement here. The subtree would get passed in to each component, and it would be able to set on the scoped state oninput, which should then update the state and trigger a render.

Then, I support undo/redo functionality, by changing the top-level state tree into a proxy, so when anything is set or delete, I store a snapshot of the whole tree, and then undo/redo buttons just setState the whole tree with the snapshot, so everything renders as normal.

This is then where I’m not sure how I would implement this snapshot/apply functionality in hookstate. I think what I would want is some way to know at the top-level (or maybe even any nested level) that there is a change down the line, so that I can know to store a snapshot. There is a Touched plugin demo that shows the parent knows when it was touched, but do those just store a one-time flag for the first change? Or is there some way to subscribe to any changes? This also makes me think about things like time travel, though I think maybe your library specifically chose to eschew this functionality.

Maybe I’m thinking about this the wrong way, and it would just be most straightforward to add a transform for saveSnapshot and have the Component oninput function just call that after state set.

Any thoughts/advice?

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:24 (17 by maintainers)

github_iconTop GitHub Comments

3reactions
avkonstcommented, Apr 11, 2020

Here is the complete matrix of use cases and re-render results.

Let’s say there are the following components:

const globalState = createStateLink({ A: 0, B: 0 }) // global state with 2 nested states

// Example 1 every child component consumes the state, but parent does not
function Example1_Parent() {
    return <><Example1_ChildA /><Example1_ChildB /></>
}
function Example1_ChildA() {
    const state = useStateLink(globalState)
    return <p>{state.value.A}</p>
}
function Example1_ChildB() {
    const state = useStateLink(globalState)
    return <p>{state.value.B}</p>
}

// Example 2 parent component consumes the state and passes nested to children, children do NOT use scoped state
function Example2_Parent() {
    const state = useStateLink(globalState)
    return <><Example2_ChildA state={state.nested.A}/><Example2_ChildB state={state.nested.B}/></>
}
function Example2_ChildA(props: { state: StateLink<number> }) {
    const state = props.state
    return <p>state.value.A</p>
}
function Example2_ChildB(props: { state: StateLink<number> }) {
    const state = props.state
    return <p>state.value.B</p>
}

// Example 3 parent component consumes the state and passes nested to children, children USE scoped state
function Example3_Parent() {
    const state = useStateLink(globalState)
    return <><Example3_ChildA state={state.nested.A}/><Example3_ChildB state={state.nested.B}/></>
}
function Example3_ChildA(props: { state: StateLink<number> }) {
    const state = useStateLink(props.state)
    return <p>state.value.A</p>
}
function Example3_ChildB(props: { state: StateLink<number> }) {
    const state = useStateLink(props.state)
    return <p>state.value.B</p>
}

// Example 4 to explain StateMemo
function Example4_SumWithoutStateMemo() {
    const sumState = useStateLink(globalState.wrap(l => l.value.A + l.value.B))
    return <>{sumState}</>
}
function Example4_SumWithStateMemo() {
    const sumState = useStateLink(globalState.wrap(StateMemo(l => l.value.A + l.value.B)))
    return <>{sumState}</>
}

const incrementA = () => globalState.nested.A.set(p => p+1)
const derementB = () => globalState.nested.B.set(p => p-1)
const incrementA_derementB = () => globalState.merge(p => ({ A: p.A + 1, B: p.B - 1 }))

Y - component rerenders on state change X - component does not rerender on state change P - component rerenders because the parent is rerendered

rerender on actions: incrementA decrementB incrementA_decrementB
Example1_Parent X X X
Example1_ChildA Y N Y
Example1_ChildB N Y Y
Example2_Parent Y Y Y
Example2_ChildA P P P
Example2_ChildB P P P
Example3_Parent N N N
Example3_ChildA Y N Y
Example3_ChildB N Y Y
Example4_SumWithoutStateMemo Y Y Y
Example3_SumWithStateMemo Y Y N

Now answering your question (“Do i need to be passing a state down from the parent? In that case, why wouldn’t that just end up triggering a re-render on the parent, which then triggers re-renders on all the children anyways?”):

  • Consume the state in a component which is best suited for this to deliver the simplest and cleanest code. For example, if you render a table from a single state with rows as substates, consume the state in the parent component and pass row states to children components (Example 2 above). This will make your code simpler (and faster) than if you would go with approach consuming the state by each row (Example 1 above)
  • Use scoped state (move from Example 2 to Example 3) when you notice nested state changes cause expensive and unnecessary re-rendering of the parent component
  • Use StateMemo for nothing more than an aggregated value, like a sum. Do not use StateMemo otherwise (I am thinking to get rid of StateMemo in the version 2 as it actually can be replaced by React.memo really, but I have not decided)

Hope it helps. Let me know.

PS: also, have you tried new documentation? I think it clearly explains the nested and scoped state differences. There is also online demo which shows the difference when the scoped state is active/inactive: https://hookstate.js.org/demo-todolist/

0reactions
avkonstcommented, May 27, 2020

Let me know if you would like the chapter extended.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Watching nested object data from parent - Stack Overflow
When I try using a watcher in my child component, it only watches the 1, 2 key levels and not when changes happen...
Read more >
nested help topics problem · Issue #3537 · osTicket ... - GitHub
I'm using nested help topics, i.e: cellphone services \ change service ... Parent levels (cellphone services and new) are disabled for ...
Read more >
Understanding Tableau Nested Project Permissions
All child projects have same permissions as parent project has. All permission changes apply to all child projects automatically; Child projects can still...
Read more >
Working with nested stacks - AWS CloudFormation
Learn about nested stacks, stacks created as a resource of another stack. ... For the first level of nested stacks, the root stack...
Read more >
Loop through Nested Tasks to get parent task's title
I have a Project and and Tasks nested under this to multiple levels (A project may have many levels of child tasks). Requirement...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found