freactal icon indicating copy to clipboard operation
freactal copied to clipboard

recompose-like local state, but with the freactal API

Open tlvenn opened this issue 7 years ago • 19 comments

Right now when you want some states, you have to create and provide it through the context using provideState and then inject it into your component with injectState. This is great when you are dealing with state containers you intend to access from multiple components and in that regards they are pretty much akin to a Mobx Store with their respective Provider and Inject. (In respect to how they are declared and injected).

However when I want to to provide a state only for a given component, I kinda want to encapsulate the state with my component without exposing it though the context.

What do you think about introducing a withState which takes the same object as provideState and create an HoC that I can use to provide state through props directly to my component ?

tlvenn avatar May 04 '17 02:05 tlvenn

I went back and forth with this, and it was almost included in the v1 release. What held me back was limiting the API surface. provideState(injectState isn't so much harder than withState(, and accepting a prop named state isn't too much harder than accessing those same keys directly off of props.

The one issue with injecting the state keys directly comes from nested state containers. As an example, consider if there were two state containers - one at the root of the tree, and one somewhere near a leaf. This second container is the one we want to use withState on. Which keys would be injected as props? Only those defined by withState? Or the keys from the root too? If the root keys aren't included, would we still see a state prop? And if they weren't included, our withState-wrapped component would re-render whenever anything in the root container was updated, regardless of whether it is used in the leaf.

I couldn't think of a good way around this, and so I left it as you see it now. I'm definitely open to suggestions for an approach here, or even just a helper function that might get you part of the way there.

divmain avatar May 04 '17 06:05 divmain

Hi @divmain , thanks for the quick feedback !

What i am trying to get with withState is an encapsulated local state that is in no way shared with anything else and therefore not part of the global shared state that freactal keep/build with the provideState. Basically this state is private, not published and therefore not accessible with injectState.

If you want your childs to be aware of it for some reason, you have to explicitly pass it down using props.

That encapsulation is crucial imho in many discreet stateful components so that freactal can truly offer a replacement for React state.

So my proposal was:

const enhance = withState({
  initialState: () => ({ counter: 0 }),
  effects: {
    addOne: () => state => Object.assign({}, state, { counter: state.counter + 1 })
  }
});

const counter = ({state}) => <div>Counter: {state.counter}</div>

export enhance(counter)

withState configuration object could take an additional property to pick the entire globalState or part of it as well so that you dont have to use injectState if you are already defining a local state using withState.

tlvenn avatar May 04 '17 07:05 tlvenn

You can accomplish something very much like this with recompose. The only substantive differences it would introduce are 1) its another dependency, 2) its just a setter-function rather than an effect, and 3) state wouldn't be captured as part of SSR.

Of course, maybe one of those three points is important to you. If it is alright with you, I'm going to leave this issue open for a bit and see if anyone else has thoughts or feedback.

divmain avatar May 04 '17 08:05 divmain

Yes using recompose / recompact withState and withHandlers combined with a pureRenderMixin can achieve something similar but I was hoping to have the same tool / api to deal with local state and global state and frankly with less ceremony. I could also leverage computed values and middleware to do interesting stuff.

And like you said, 2) and 3) are kinda interesting as well.

tlvenn avatar May 04 '17 10:05 tlvenn

The more I think about this, the more I think this might be better as a separate project. I'm not decided on that though, and it is quite possible that the two could share some common code. Leaving this issue open until that is decided.

divmain avatar May 27 '17 08:05 divmain

I've found myself writing small helpers to do simplistic local state as well, namely for SSR.

I don't really want to go back to recompose and lose that.

ericclemmons avatar Jun 01 '17 02:06 ericclemmons

Having thought about this for a bit longer, I'm going to disagree with my May-27-self and say this would be a good addition. I'm not sure of the ultimate implementation yet, but I do see a lot of utility in providing the same API for both use cases.

divmain avatar Aug 03 '17 19:08 divmain

Hi @divmain , is the light of the recent 2.0 release, can you update us if this is still on the roadmap ? Thanks !

tlvenn avatar Dec 20 '17 19:12 tlvenn

Would love to see this as well.

agurtovoy avatar Feb 27 '18 03:02 agurtovoy

Just as an idea, perhaps this can be as simple as adopting (and implementing) a convention that any state properties that start with underscore are private and are accessible to the immediately wrapped component only?

agurtovoy avatar Feb 27 '18 03:02 agurtovoy

not that anyone asked me but: wouldn't the react state/setState serve this function just as well?

bingomanatee avatar Feb 27 '18 08:02 bingomanatee

@bingomanatee No, that's a valid point! One issue is just switching back and forth between different styles of code, which is why people use things like recompose's withState et al. Then there is a use case of private state mixed in with shared state within a single freactal component. Then there is SSR and the ability to test state and UI in isolation. if none of the above is of a particular concern/interest, then React's builtin state management is a totally adequate answer.

agurtovoy avatar Feb 28 '18 00:02 agurtovoy

I'm using component composition for transient state. for instance to debounce API calls from field updates.

In my experience local sate rarely stores well with global state. If you can abstract a state from a global model its because its about something you don't need global persistence on.

for instance: assume you have a situation where you have a dashboard with four panels each of which has a dropdown that lets you choose between a chart and a carousel. Each carousel has a current page number.

If you were to store the page number and dashboard type in a global model it makes sense. However if you were to localize the page number locally then serializing and de-serializing the page number globally is non-sensical. you could have two carousels open to different pages at which point you'd record two different carousel numbers, one for each view.

I'd have trouble coming up with a consistently applicable plan to serialize local state that has no reference to its context; at which point it becomes just a semantic tweak of global state.

I think its tough to come up with a use case for a localized state detached from a global model and a good candidate for serialization/SSR.

bingomanatee avatar Feb 28 '18 01:02 bingomanatee

One case that I ran into recently was Draft.js internal state. No one but the editor component itself cares about it, but if you don't serialize it from the server, you'll get a warning about mismatching keys (https://github.com/facebook/draft-js/issues/1242).

agurtovoy avatar Feb 28 '18 04:02 agurtovoy

I can see how you would want to store that in session. Which is why I'm not sure what advantage you get from keeping it out of global state. This is one of the issues I have with sub-component states in freactal; I'm not sure how they serialize well when they can (in a context) override each others' values.

bingomanatee avatar Mar 04 '18 21:03 bingomanatee

No advantage other than the clarity of code/intent.

The sub-component states serialize correctly because each component simply pushes its raw state into a global array during SSR rendering. There is no interpretation/consolidation of state happening at that point. Overriding happens when freactal prepares a copy of the state that it passes to render/effects.

agurtovoy avatar Mar 04 '18 23:03 agurtovoy

so say you have two editing components on the same page - if they've both been set to record in the same fashion wouldn't they constantly override each others' state?

bingomanatee avatar Mar 05 '18 00:03 bingomanatee

Yep, they won't override each other's state; connecting them to the parent state is trickier, though. I find myself resorting to onChange callbacks most of the time rather then going through the parent effects.

agurtovoy avatar Mar 05 '18 23:03 agurtovoy

Here is kind of my thinking of all of this at this point. Freactal is kind of going back to the old backbone model of state, hooks, events and updates.

Any time you have a state which is not anchored in the global data model -- and I'm not disputing that has its place as in a mouseover activating a tooltip -- and the state is transient enough not to be anchored to the global model -- it belongs in the classic React state system which behaves as a localized flag and doesn't persist.

Situations where you are editing a thing belongs in a pool of edited versions. I.e., you have a collection of real values from the db in State, and a collection of user edits that refer to real values (by id) -- and that hopefully you persist locally, or even asynchronously in real time to the database as "pending" versions of your data.

Then, when the app loads again, maybe because the user refreshed the page, if your UI class says, "Oh I'm opening a page, let me get the data from the state AND check local history for related edits" you can re-sync your user state prior to your first render.

--- apologies for first version -- page <->panda. autocorrect.

For other use cases see Schrödinger's Cat.

bingomanatee avatar Mar 06 '18 01:03 bingomanatee