unstated-next
unstated-next copied to clipboard
[Question] The best practice to combine containers to have it as "global" state
<Container3.Provider>
<Container2.Provider>
<Container1.Provider>
<App />
</Container1.Provider>
</Container2.Provider>
</Container3.Provider>
I think above there is antipattern. How to do it in the best manner?
https://github.com/jamiebuilds/unstated-next/issues/10
Something like this?
store.js
// i'm using ink so this is regular node not ES6 imports
const React = require("react");
const Bank = require("./Bank");
const Counter = require("./Counter");
const compose = (...containers) => {
const nest = (containers, children) => {
if (containers.length > 0) {
const [first, ...rest] = containers;
return <first.Provider>{nest(rest, children)}</first.Provider>;
}
return children;
};
return {
Provider: ({ children }) => {
return nest(containers, children);
}
};
};
module.exports = compose(Bank, Counter);
app.js
const Store = require('./store')
// ...
<Store.Provider><YourComponent /></Store.Provider>
Maybe you can just create a store container?
const useA = () => {//...}
const useB = () => {//...}
const composeHooks = (...hooks) => () => hooks.reduce((acc, hook) => ({ ...acc, ...hook() }), {})
const Store = createContainer(composeHooks(useA, useB))
const App = () => (
<Store.Provider>
//...
</Store.Provider>
)
WHY?!
In my case I want to create a Container that composes and extends other Containers
const useA = () => {//...}
const useB = () => {//...}
const ContainerA = createContainer(useA)
const ContainerB = createContainer(useB)
const useC = () => {
cont { somethingFromA } = ContainerA.useContainer()
cont { somethingFromB } = ContainerB.useContainer()
const somethingFromC = somethingFromA + somethingFromC
return { somethingFromC }
}
const ContainerC = createContainer(useC)
So to use ContainerC you need the providers of A and B
<ContainerA.Provider>
<ContainerB.Provider>
<ContainerC.Provider>
<App />
</ContainerC.Provider>
</ContainerB.Provider>
</ContainerA.Provider>
So I use this function to combine the deps:
import React from 'react'
export const addContainerDeps = (container, ...deps) => {
if (!deps) return container
const CurrentProvider = container.Provider
container.Provider = props =>
deps.reduce(
(combinedDeps, Dep) => <Dep.Provider>{combinedDeps}</Dep.Provider>,
<CurrentProvider>{props.children}</CurrentProvider>
)
return container
}
In this way:
const ContainerC = addContainerDeps(
createContainer(useC),
ContainerA,
ContainerB
)
<ContainerC.Provider>
<App />
</ContainerC.Provider>
Hmm this is just confusing in my opinion, its not very declarative
getting the values from A + B passed into C via a render would be much cleaner
Does C actually do anything special?
could you not just write a custom hook wrapping useMemo if its intensive ?
function useComputedValuesFromAAndB() {
const { value: aValue } = ContainerA.useContainer()
const { value: bValue } = ContainerB.useContainer()
return aValue + bValue
}
function MyComponent() {
const computedValue = useComputedValuesFromAAndB();
return <div>{computedValue}</div>
}
It is great @tancc https://github.com/jamiebuilds/unstated-next/issues/55#issuecomment-557970792
And I add some code to avoid key conflict.
const useA = () => {//...}
const useB = () => {//...}
const composeHooks = (...hooks) => () => hooks.reduce(
(acc, hook) => {
const hookObj = hook();
if(Object.keys(acc).every(key => hookObj[key] === undefined)) {
return {...acc, ...hookObj}
} else {
throw new Error('there exist same key in multiple hooks');
}
}, {}
)
const Store = createContainer(composeHooks(useA, useB))
const App = () => (
<Store.Provider>
//...
</Store.Provider>
)
It is great @tancc #55 (comment)
And I add some code to avoid key conflict.
const useA = () => {//...} const useB = () => {//...} const composeHooks = (...hooks) => () => hooks.reduce( (acc, hook) => { const hookObj = hook(); if(Object.keys(acc).every(key => hookObj[key] === undefined)) { return {...acc, ...hookObj} } else { throw new Error('there exist same key in multiple hooks'); } }, {} ) const Store = createContainer(composeHooks(useA, useB)) const App = () => ( <Store.Provider> //... </Store.Provider> )
@helongbin @tancc excuse me,I guess you create a shallow copy of all state, and inject to the root App. but there is a problem when your local state A changed, it will re-trigger the component which just need state B
You can checkout my idea: react-hook-svs. Highlights:
- Get service output immediately in the hosting component. You no longer need to wrap your top component with provider HOC to get service output in it.
- One service can consume another, even when they are in the same component. react-hook-svs provides a consistent way to consume service.
-
Service providers(basically React context) are composited together and will be injected into a JSX subtree at once. No longer provider hell. Checkout this demo:
-
Service abstraction.
- Normal React hooks abstraction: the caller of a hook can't know whether the hook call other hooks in it. The nesting hook call is abstracted away by the parent hook.
- Beside normal React hooks abstraction,
react-hook-svs
gives you service abstraction: SvsA can run(instead of consume) SvsB inside it, but the users of SvsA will not feel the existance of SvsB: SvsB will not be visible in the scope and react context. SvsA can re-export and re-name the output of SvsB to make it visible. Checkout this demo:
You know, I've come back to this problem several times over the last year or so. And my conclusion is that the most maintainable method is to just ignore the "ugliness" of a deeply nested tree and just roll with it:
<ContainerA.Provider>
<ContainerB.Provider>
<ContainerC.Provider>
<ContainerD.Provider>
<ContainerE.Provider>
<ContainerF.Provider>
<App />
</ContainerF.Provider>
</ContainerE.Provider>
</ContainerD.Provider>
</ContainerC.Provider>
</ContainerB.Provider>
</ContainerA.Provider>
Once you get past the ugliness, it's actually quite liberating. It clearly shows the hierarchy of the different providers and doesn't require writing any logic with reduce (or any logic at all!).
It's my opinion that this "ugly" way of doing things might actually be the most maintainable after all. What's the point of making something look "nice" and "elegant" if it becomes a huge pain to maintain? Are we engineers or are we artists first?
I welcome all downvotes 🤗
@adrianmcli I think Dan Abramov had an article on his blog here about this. I agree with this. I don't like it one single bit though. But I agree.
Currently I have thrown out redux and working with unstated. What I did with the stores (containters)
class ContainerA extends Container {}
const containerA = new ContainerA();
export default containerA;
class ContainerB extends Container {}
const containerB = new Containerb();
export default containerB;
const rootStore = [ContainerA, ContainerB]
<Provider>
<Subscribe to={rootStore}>
<App />
</Subscribe>
</Provider>
and it works
@hck1205 em...there is a problem with unstated, if you use unstated which return multi states, every setstate will trigger the whole page rerender... even you do not change other state...
@hck1205 em...there is a problem with unstated, if you use unstated which return multi states, every setstate will trigger the whole page rerender... even you do not change other state...
Would that be a problem if app is set with react routers? If so, probably i should subscribe stores in separate component.
But is it okay to access stores directly in component by using Import?
@hck1205 em...there is a problem with unstated, if you use unstated which return multi states, every setstate will trigger the whole page rerender... even you do not change other state...
Would that be a problem if app is set with react routers? If so, probably i should subscribe stores in separate component.
But is it okay to access stores directly in component by using Import?
if your app has some complex state, like component A use stateA, component B use stateB, but all state come from the same unstated, you will need to seprate into multi state, if you do not want the unnecessary rerender
@hck1205 em...there is a problem with unstated, if you use unstated which return multi states, every setstate will trigger the whole page rerender... even you do not change other state...
Would that be a problem if app is set with react routers? If so, probably i should subscribe stores in separate component. But is it okay to access stores directly in component by using Import?
if your app has some complex state, like component A use stateA, component B use stateB, but all state come from the same unstated, you will need to seprate into multi state, if you do not want the unnecessary rerender
no worries, i have separated root store into multiple stores and distributed them to components where requires to access to the stores. and have tested rendering issues. I think it works fine so far.
Maybe you can just create a store container?
const useA = () => {//...} const useB = () => {//...} const composeHooks = (...hooks) => () => hooks.reduce((acc, hook) => ({ ...acc, ...hook() }), {}) const Store = createContainer(composeHooks(useA, useB)) const App = () => ( <Store.Provider> //... </Store.Provider> )
it seems you must subscribe the whole context while you only want to subscribe useA . and the comonent will rerender when useB hook is trigger but the comonent may not use the data in useB hook