Skip to content
This repository has been archived by the owner on Jan 27, 2021. It is now read-only.

Questions and thoughts #30

Closed
jcheroske opened this issue Jul 6, 2017 · 7 comments
Closed

Questions and thoughts #30

jcheroske opened this issue Jul 6, 2017 · 7 comments

Comments

@jcheroske
Copy link

jcheroske commented Jul 6, 2017

This lib looks fantastic. I have some questions about best practices when using it though. When building a fractal component, what is the preferred way for the parent to interact with the children? Do you ever tinker with child state from a parent reducer? Like if the child needs to be reset back to initial state, would you do that in the parent reducer. Or, would you make the child component a controlled component and have the parent interact with it through value and onValueChanged props, for example? Or maybe there's another way that parent->child communication should happen?

The other thing I'm wondering about is redux-logic support. I know it's not the most popular side-effect lib, but if you haven't looked at it, it's wonderful. No strange generator or observable syntax to deal with. You can write your side effects using async/await if you want, and the finished code ends up reading so clean. I've used sagas and epics, and I'm moving all of that code over to logics now. Since all logics are created with the createLogic function, it might be quite easy to wrap that in some way to add namespacing support.

Anyway, thank you for your work here.

@mpeyper
Copy link
Contributor

mpeyper commented Jul 7, 2017

Hi @jcheroske,

This lib looks fantastic.

Thanks :)

When building a fractal component, what is the preferred way for the parent to interact with the children?

There doesn't appear to be one single, correct-in-all-cases approach emerge as yet. Generally speaking, I find the parent components don't really need to interact much with the children, other than to provide some basic props, as they are able to take care of their own state and actions.

Do you ever tinker with child state from a parent reducer? Like if the child needs to be reset back to initial state, would you do that in the parent reducer. Or, would you make the child component a controlled component and have the parent interact with it through value and onValueChanged props, for example? Or maybe there's another way that parent->child communication should happen?

redux-subspace was designed so that the parent controls the bounds of the subspace (i.e. where in the store it lives and what the namespace is), but the children are in control within the subspace. As such, I would not advocate tinkering with the child's state directly from the parent.

Using a value and onValueChanged pattern may work, but at some point, keeping the child's state in sync with what parent component is passing in will likely become an issue (but if you do successfully do it feel free to share your solution and tell me I was wrong).

My advice would be to use global actions from the parent and have the child handle them in it's reducer. This way the parent is controlling what it wants to happen, and the child is controlling how it will happen.

The other thing I'm wondering about is redux-logic support...

I haven't heard of redux-logic until just now, so I'm not very familiar with it. I've had a (very) brief look at it but looks like createLogic just returns a plain object with the process (and other hooks) on it, so we should be able to wrap to prefix the type and inject subspaced getState and dispatch calls to the hooks, at least for a single level. I'm not sure yet how this would go for deeply nested subspaces.

Usually, the things being wrapped (components, reducers, sagas, etc.) can be combined in some way at at each level of nesting, but I can't see anyway to do that with redux-logic, so when adding them to the middleware at the top level, you would need to know exactly which levels the originating action passed through to get the namespacing right. Possibly this could be mitigated by having a convention of wrapping the logic each time it was exported at a relevant namespacing layer, but I'm not familiar enough with it yet to say for certain.

@jcheroske
Copy link
Author

My advice would be to use global actions from the parent and have the child handle them in it's reducer. This way the parent is controlling what it wants to happen, and the child is controlling how it will happen.

Let's say a child component has a reset action. Is there a way for a parent to fire a scoped action that would only target the correct child? I can see how a given component can fire actions that are scoped to itself. I'm wondering if it's possible to also fire scoped actions to children?

@mpeyper
Copy link
Contributor

mpeyper commented Jul 11, 2017

Let's say a child component has a reset action. Is there a way for a parent to fire a scoped action that would only target the correct child? I can see how a given component can fire actions that are scoped to itself. I'm wondering if it's possible to also fire scoped actions to children?

@jpeyper and I were just discussing this yesterday.

Theoretically, you can just raise an action with the namespace prefix of the child, and the namespaced reducer will unwrap it an let it through, e.g.

<SubspaceProvider mapState={/* whatever */}} namespace="child">
    <ChildComponent />
</SubpsaceProvider>

...

// from parent level
dispatch({ type: "child/CHILD_ACTION" /*, ...*/ })

Obviously, this isn't ideal as it requires the developer to know about the namespacing prefix and to use it appropriately. I think providing a wrapper would be much nicer, i.e.

// from parent level
dispatch(namespacedAction({ type: "CHILD_ACTION" /*, ...*/ }, "child")

@jcheroske
Copy link
Author

And this really is the rub with the current namespacing libraries. They work, but the abstractions have not captured the all of the common use cases, and so they leak. Having scoping functions for all the different elements (reducers, components, action creators) would not only tighten up the abstractions, but would encourage best practices.

Another possible leak is around getting a child reducer's initial state. For example, when adding a reducer dynamically you usually want to get its initial state and push it onto the array that holds the state. How is this done? Do you just pass undefined into the reducer (bad smell maybe?) or is there an explicit way to get the initial state that is more self-documenting? I don't have strong feelings, but there probably should be a documented way of doing it.

@mpeyper
Copy link
Contributor

mpeyper commented Jul 11, 2017

Having scoping functions for all the different elements (reducers, components, action creators) would not only tighten up the abstractions, but would encourage best practices.

I think having action wrappers to help with common abstractions is a great idea.

I think a namespacedAction wrapper, where it just prefixes the action (and/or action creator?) with the provided namespace would be easy to add without much effort.

Other abstractions @jpeyper and I discussed which would be great, but I don't know how much effort is involved (they're not as trivial as they sound), are:

  • childAction - a direct child of the namespace will handle the action
    • e.g. childAction({ type: "RESET" }) when dispatched from namespace "level1" would be accepted by reducers namespaced to "level1/level2", "level1/other", etc. but not by reducers namespaced to "level1", "level1/level2/level3", "other", etc. or non-namespaced reducers
  • descendantAction - a descendant of the namespace will handle the action
    • e.g. descendantAction({ type: "RESET" }) when dispatched from namespace "level1" would be accepted by reducers namespaced to "level1/level2", "level1/level2/level3", etc. but not by reducers namespaced to "level1", "other", etc. or non-namespaced reducers
  • parentAction - The direct parent of the namespace will handle the action
    • e.g. parentAction({ type: "RESET" }) when dispatched from namespace "level1/level2/level3" would be accepted by reducers namespaced to "level1/level2" but not by reducers namespaced to "level1", "level1/level2/level3", "level1/other", etc. or non-namespaced reducers
  • ancestorAction - The direct parent of the namespace will handle the action
    • e.g. ancestorAction({ type: "RESET" }) when dispatched from namespace "level1/level2/level3" would be accepted by reducers namespaced to "level1" and "level1/level2" but not by reducers namespaced to "level1/level2/level3", "level1/other", "other", etc. or non-namespaced reducers

Another possible leak is around getting a child reducer's initial state. For example, when adding a reducer dynamically you usually want to get its initial state and push it onto the array that holds the state. How is this done?

Just to clarify, adding reducers dynamically is not something redux-subspace does, nor do I believe it is something it should be responsible for. That is something else's job.

That said, I have some insight into how the initial state is determined when adding reducers dynamically to redux. There is a great Stack Overflow answer by Dan Abramov about how to dynamically inject reducers which I used as the inspiration for our dynamic reducer solution.

When replaceReducer is called, the current state is passed into the new reducer and the resulting state becomes the new state. As the new reducer doesn't have a node in the current state tree, it's current state is undefined so the default behaviour kicks in to initialise the state to it's initialState for the new state. So

Do you just pass undefined into the reducer...?

We don't, but redux does.

@jcheroske
Copy link
Author

I'm going to close this, as it's a bit stale and you seem to be moving forward with many amazing things. I'll take a look at Dan's answer re: reducer injection. Did you use any of the dynamic reducer libs out there, or totally roll your own?

@mpeyper
Copy link
Contributor

mpeyper commented Sep 21, 2017

Did you use any of the dynamic reducer libs out there, or totally roll your own?

Totally, rolled my own

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

2 participants