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

context -> recoil / recursive state #498

Open
atanasster opened this issue Jul 27, 2020 · 13 comments
Open

context -> recoil / recursive state #498

atanasster opened this issue Jul 27, 2020 · 13 comments
Labels
question Further information is requested

Comments

@atanasster
Copy link

Hi,

I am trying to re-create a react context implementation now using recoil and want to create a recursive state implementation:

  type Control = {
   value: string | Controls;
 } 
 type Controls = Record<string, Control>;

In the context API, I do have two context providers over the same context - one for the outer object, and one created for each child that has recursive value as Controls.

For recoil, I am trying to accomplish something similar (with an inner RecoilRoot?) and was wondering if I could somehow change the setter/getter for a selector within the RecoilRoot context?

LMK if posting links to the recoil and context API source will help

@drarmstr drarmstr added the question Further information is requested label Jul 27, 2020
@drarmstr
Copy link
Contributor

Sorry, could you explain what you are trying to accomplish?

@atanasster
Copy link
Author

Sorry, I am not very good at explaining. I will try a different approach to explaining my use-case, hope it gets clearer.

In a documentation-type platform, I have the following structure

document(page)-story(An example)-controls(props for each example)

Some types of ‘controls’ can have controls of their own (array-type control, or object-type control).

In the context API, I have two context providers, based on the same context:

  1. For the ‘story’: https://github.com/ccontrols/component-controls/blob/d1543b6f7b10d663d04528168c56db3c0f553ea1/core/store/src/state/context/controls.tsx#L19

  2. For ‘controls’ that have their own child controls https://github.com/ccontrols/component-controls/blob/d1543b6f7b10d663d04528168c56db3c0f553ea1/core/store/src/state/context/controls.tsx#L52

The first one is created in the root context state
https://github.com/ccontrols/component-controls/blob/d1543b6f7b10d663d04528168c56db3c0f553ea1/core/store/src/state/context/StateRoot.tsx#L53

The second one is created in array/object-type controls to wrap their (recursive) children
https://github.com/ccontrols/component-controls/blob/d1543b6f7b10d663d04528168c56db3c0f553ea1/ui/editors/src/ArrayEditor/ArrayEditor.tsx#L63

What this provides me is the ability to simply consume the context on each child control, regardless if its part of the main story object, or part of control itself.

Here is a small video, showing the above at work (array-type editor that has child editors of its own for each row):
controls

@drarmstr
Copy link
Contributor

Ok, so you have controls with either simple state or state composed of compound nested controls. Still not clear what the specific issue is?

@atanasster
Copy link
Author

atanasster commented Jul 28, 2020

Thanks for looking into this @drarmstr - yes, that's correct - nested/recursive state.

The specific issue is that using recoil selectors, I can not update 'different' state with the same selector.

Here is my hook, that returns a control and a setter (it is used by all editors that need to edit some cotrol value): https://github.com/ccontrols/component-controls/blob/d1543b6f7b10d663d04528168c56db3c0f553ea1/core/store/src/state/recoil/controls.tsx#L45

The setter needs sometimes to update the simple state, sometimes needs to update the parent nested controls state. With context API, I have two different context providers, however, in recoil I do not see a way of nesting selectors.

I am not sticking to have the exact same implementation (although it would be nice to not have to modify my code using the state setter hook too much), so even if its a different approach, do you have any example where updating recursive state with recoil?

@drarmstr
Copy link
Contributor

You may want to break up the controls state into individual atoms per control using something like atomFamily(), so that updating one control won't cause components subscribing to any other control to re-renrender. The setter should be able to either set the state of the control's atom to either the simple or the compound state. I'm not able to go through all your code, so I'll make some assumptions based on your question - if your intention is for the nested controls to have their own state, and for the parent/compound controls to contain those nested controls, then for compound state you wouldn't be storing the nested state directly, but instead some handle or Recoil atom/selector referencing the nested control. You could use a selector to compute the compound state for the rendering components and the selector could then lookup and get the actual state of the nested controls. In this way the components rendering a control would only subscribe to updates from the compound control and the specific nested controls that it uses.

@atanasster
Copy link
Author

@drarmstr - thanks, but the atomFamily is a bit out of the scope here (to subscribe for each individual control/property value). Once i get it to work for all controls on the current level i could indeed split up into family selectors.

My issue and still from your response - those are different atoms/selectors. What i need is to subscribe to some state without the editors even knowing if they are editing main-level state or any of the nested states. As i mention - react context allows for multiple providers, and the editors simply consume - without ever knowing if they are consuming a main-level provider or a sub-level provider.

Sorry but i still dont see in your answer how the editors can abstract from which selectors/atoms they are consuming. If you could post even some mock up code that would help - really no need to go through my code, i can even create a very simple use-case of nested state if that would help to get it going.

@atanasster
Copy link
Author

I am using the term nested as it seems you preferred it, but this can be confusing. Recoil can of course deal with simple nested state - the issue is more appropriately called recursive i think - the same state that can keep being repeated in children. Sorry if any confusion come out, I dont mind calling it any which way as long as we have the same thing in mind :)

@drarmstr
Copy link
Contributor

drarmstr commented Jul 29, 2020

The following is super simple and contrived, but is something like it helpful?

type ControlState = {
  type: 'string',
  value: string,
} | {
  type: 'list',
  values: ReadOnlyArray<ControlID>,
};

const controlState = atomFamily<ControlState, ControlID>({
  key: 'Controls',
  default: {type: 'string', value: ''},
});

Then you could get and manipulate the atom for each control. either as a simple value:

  const setControlState = useSetRecoilState(controlState(controlID));
  setControlState({type: 'string', value: "Hello World"});

or a compound value:

  setControlState({type: 'list', values: [firstControlID, secondControlID]});

A selector could provide derived state to abstract the results of the different types. Again a very contrived example:

const abstractedControlState = selectorFamily<string, ControlID>({
  key: 'AbstractedControl',
  get: controlID => ({get}) => {
    const control = get(controlState(controlID));
    
    switch (control.type) {
      case 'string':
        return control.value;
      case 'list':
        return get(waitForAll(control.values.map(abstractedControlState)).join(', ');
    }
  },
});

@atanasster
Copy link
Author

atanasster commented Jul 29, 2020

Thanks a lot @drarmstr i will try to set up a use case sample repo and will start It with your example above - i am afraid At first look that its not what i need(hard-coding the ‘list’ type, as i need abstraction at the editors level(where the state is consumed) - some are arrays, some are objects and more editors can be created) but It will give us a common base to try to find a solution.

One solution i tried today was to create dynamically new selectors and pass a selector to each editor from its parent(either from the main state or another editor being the parent). Its also convoluted but works to some extent, except unique selector names are cached. It feels i would really like to have RecoilRoot or other container to be able to change/update the behavior of existing selectors by name(ie - override selectors/atoms just for the specific conext) - this approach makes it really easy and natural to create such recursive state with context api.

Thanks again, will post more Tomorrow..

@atanasster
Copy link
Author

@drarmstr - #298 the idea of context bridging might be exactly what I am also looking for, no? An addition where we could override the behavior of selectors/atoms.

I am also still trying to make a real test case with your code but its not really natural(or even working atm).

@drarmstr
Copy link
Contributor

The argument I've seen for context switching so far is mostly when you are using nested React renderers and need to bridge the nested <RecoilRoot> to use the same shared state. It isn't for fine-granularity overriding of individual atoms/selectors.

@atanasster
Copy link
Author

Thanks @drarmstr - any other ideas for making this not hard-coded on the state side? Do you think my other attempt(create selectors on the fly and pass them from parent to child editors) can have some legs? I feel i am really close to be able to use recoil in production. Do you have a list of items to address, i would add To following and maybe we can consolidate my items:

  • this thread here (ability to override selector behavior within some react context), use case; recursive state.
  • ability to reset state of selector to a new value (possibly within the selector get method?) that updates all instances using the selector. Use case; notifications of state update from separate process.
  • ability to pass initialState by value (or any other way to differentiate state changes from within recoil vs external changes), use case: external state refreshed by hmr or other external means, as well as internal selector set method.
  • the cannot update state ... - which i see is fixed in the code, so maybe just release will fix it for me.

I have a few other items that are now great, thanks to everyone involved.

  • ssr works great so far
  • typescript interfaces are close to perfect

@hosseinmd
Copy link

How about create an atom in component and give it to context like:

const context=createContext();
function Provider({children}){
   const [state]=useState(()=>atom({...}))

   return <context.provider value={state}>{childern}</context.provider>
}

Now in children components:

function Comp(){
const atomState= useContext(context)
....
}

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants