unstated-next icon indicating copy to clipboard operation
unstated-next copied to clipboard

[Question] The best practice to combine containers to have it as "global" state

Open kspacja opened this issue 4 years ago • 17 comments

<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?

kspacja avatar Nov 03 '19 16:11 kspacja

https://github.com/jamiebuilds/unstated-next/issues/10

AjaxSolutions avatar Nov 03 '19 17:11 AjaxSolutions

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>

mgutz avatar Nov 12 '19 22:11 mgutz

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>
)

tancc avatar Nov 25 '19 03:11 tancc

WHY?!

export-mike avatar Dec 06 '19 00:12 export-mike

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>

hernanonzalo-toast avatar Dec 18 '19 16:12 hernanonzalo-toast

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>
}

export-mike avatar Dec 19 '19 00:12 export-mike

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>
)

robinhe avatar Feb 02 '20 12:02 robinhe

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

wind4gis avatar Feb 20 '20 07:02 wind4gis

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: Edit react-hook-svs
  • 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: Edit react-hook-svs

csr632 avatar Apr 09 '20 18:04 csr632

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 avatar Apr 13 '20 04:04 adrianmcli

@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.

sushruth avatar May 15 '20 03:05 sushruth

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 avatar May 18 '20 13:05 hck1205

@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...

wind4gis avatar May 20 '20 03:05 wind4gis

@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 avatar May 20 '20 03:05 hck1205

@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

wind4gis avatar May 20 '20 08:05 wind4gis

@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.

hck1205 avatar May 20 '20 08:05 hck1205

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

ldqUndefined avatar Jul 20 '21 08:07 ldqUndefined