Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
kspacja opened this issue Nov 3, 2019 · 17 comments

Comments

@kspacja
Copy link

kspacja commented Nov 3, 2019

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

@AjaxSolutions
Copy link

#10

@mgutz
Copy link

mgutz commented Nov 12, 2019

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>

@tancc
Copy link

tancc commented Nov 25, 2019

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

@export-mike
Copy link

WHY?!

@hernanonzalo-toast
Copy link

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>

@export-mike
Copy link

export-mike commented Dec 19, 2019

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

@robinhe
Copy link

robinhe commented Feb 2, 2020

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

@wind4gis
Copy link

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

@csr632
Copy link

csr632 commented Apr 9, 2020

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

@adrianmcli
Copy link

adrianmcli commented Apr 13, 2020

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 🤗

@sushruth
Copy link

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

@hck1205
Copy link

hck1205 commented May 18, 2020

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

@wind4gis
Copy link

@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
Copy link

hck1205 commented May 20, 2020

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

@wind4gis
Copy link

@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
Copy link

hck1205 commented May 20, 2020

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

@ldqUndefined
Copy link

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

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

No branches or pull requests