-
-
Notifications
You must be signed in to change notification settings - Fork 15.3k
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
Investigate using Redux for pseudo-local component state #159
Comments
Note that I think all of this can be implemented totally independent from Redux in a separate repo. Anybody wanna try? |
The tricky part of course is to figure out when (and whether) to erase such state. This may be a bad idea after all, see @jimfb's comments in this gist. But I still think we ought to give it a try.. |
I think a great first approximation is to make it work so that the “pseudo-local” state is always destroyed when the component unmount. The set of “actions” would be |
Here's something interesting. This is totally implementable outside core as long as the user doesn't forget to include our “local state reducer” in their global reducer. Doesn't this sound similar to what I wanted in #113? It's the same “third party” state and “third party” actions all over again. It sounds like there's a special sort of functionality many plugins want to have: the ability to write to a custom slice of sub-atom that user is not managing explicitly. @acdlite |
@gaearon we're almost doing that in panels (a runtime to run independent apps with a very particular (mobile focused) UX). However, because each app is meant to be independent, we isolate its flux instance in what we call contexts which is part of the runtime's atom. I should be in a position to show you what I just described in real code sometime around the conference if you're interested. I'm interested to see how this unfolds and how I can help shape it. :) |
Cool, see you at the conf then! |
Definitely :) |
Similar ideas in clojurescript http://blog.circleci.com/local-state-global-concerns/ |
After actions as pure functions, initial state, state-less flux, another stuff I'm using in http://github.com/steida/este Do we follow the same logic steps? I'm calling that pattern globalized local state: https://github.com/steida/este/blob/13c59be76c39ecfabbf6f8ada17e8ffde0e59cbf/src/client/components/editable.react.js#L101 |
I don't know that it makes sense to couple transient state with the component. Maybe I'm misunderstanding but really I want transient state that potentially spans multiple components.... |
That's exactly what I'm suggesting. :-)
|
My suggestion for this type of an API would be
The reason I like that slightly better is that you can use actions like normal, state is passed through props and you get the replay functionality. You also have the nice thing of not needing setState. The child components also don't need to change based on whether they are using global or local state. |
I wonder if this could be used with the upcoming Stateless Functions feature in React? |
Either one of the proposals could be used with stateless functions. As long as everything is passed through props, which it is. |
@gaearon I have created a successful experiment with pseudo-local state. It is located here I will split it out into a separate repo shortly. The api closely follows my proposal mentioned earlier. |
In the app I changed the edit text of |
Current Code based on idea from this thread: reduxjs/redux#159 and initial experiments from https://github.com/taylorhakes
Following ideas from this discussion I've made a repo to support redux at component level: redux-component-state. The current version is based on requirements of a specific project I'm working on but I want to add more features in the next weeks. I'd like to have feedbacks and suggestions on it :) |
We have been having a related discussion over in anthonyshort/deku#218. IMO the custom key generation thing is critical, so critical that I almost thing it might be a good idea to eliminate the default of randomly generating them. I also don't really like the idea of a EDIT: I'd also add that TBH to me, the entire concept of component <-> state mapping, feels like it is a bit of a red herring. It is something that is very natural to think about, but might actually be deeply impractical to actually use? I don't have a great proposal for an alternative, but it is a nagging feeling that I have, that state should really not be considered in terms of the UI tree at all. |
A good example to think through I think is a dropdown (as is discussed in the linked thread). A dropdown has a local state (open/closed), but that state is also global, because a click anywhere other than the dropdown should close that dropdown. Usually people will do something like document.addEventListener('click') from inside their nested dropdown component, but that is a global listener for what was supposed to be a piece of local state. My best thought at the moment for this is to create a higher-order duck for dropdowns that can be parameterized with your application's 'uncaught click on document' action. This doesn't solve the problem of lifecycle management or anything like that though. |
The dropdown may be a bit of a red herring. The problem with I think that's outside the bounds of application state, which is how I use redux. The selected item in a list is application state. The fact that the list is expanded is component state. It's all about what state comes into the component externally. No one upstream of my component tree will tell my dropdown to open, so why would they need to know about it? Maybe some redux-y or react-redux-y wrappers around local state would be better? Something to give it similar behavior to my application state, but applied and only accessible at the local level. |
@timdorr I think you are right about the use of However, why do you say noone upstream of your component might tell it to open? Also, i'm not sure I understand your distinction between 'application state' and 'component state' here. It is very possible that someone outside of your dropdown may need to know about it. Sometimes you want to move things around slightly when a dropdown is open, or do other things like that. I think, in general, making distinctions between types of state is a very slippery slope, and the more we can unify it all into redux's global state atom the better. |
If someone else needs to know about your dropdown state, then it's no longer local component state and is, instead, application state and should be managed by redux. However, if we're thinking declaratively, the fact that the state indicates an open dropdown is just an implementation detail of the component. If you need to move things around, that sounds more like a CSS problem than an application problem. Or at the very least, that would be local state contained at a higher, but not top-level, container. |
@timdorr Ya, I think what i'm saying is that there isn't really such a thing as local component state, and that trying to create and enforce strong component state boundaries is a mistake. Also, in relation to the document click issue, while I do think you're right about that in particular, I think the general problem is still valid. Your application has application-specific but global events that take place, and your sub-components may want to respond to those events, so I do think you need a way of parameterizing things like dropdowns with event types expressed in the language of your application. Examples might be closing modals/dropdowns on route change, or similar. |
I disagree. There is inherently local state because of the actual rendered view (DOM, in this case) having some state itself. React does a great job of normalizing the behavior of that state, but there is an inherent externality to it. In particular, there is state we cannot manage, such as the value of file inputs. I don't think redux should be the do-all, end-all source of state in the entire javascript environment. It doesn't handle side effects at all, so it cannot be the ultimate state authority. It's best for managing application state, so it should be limited to that use. Trying to map and synchronize every little bit of state throughout your components and the DOM back to a single location is undoubtedly never going to be performant and suffer from quirks of that translation. I happen to enjoy the boundaries of component and application state, as they are rarely intermingled more than one level up my component tree. My smart components act as a gatekeeper to dumb components that don't concern themselves with external effects. They provide APIs for my containers to compose them together logically. YMMV but it's been working great for me. |
I was thinking whether it's possible to make a store enhancer that would let you "branch" ephemeral stores that have the standard |
e.g. import { compose, createStore } from 'redux';
import ephemeralize, { mountStore, unmountStore } from 'redux-ephemeral';
let finalCreateStore = ephemeralize(createStore);
let store = finalCreateStore(reducer);
class SomeComponent {
componentWillMount() {
this.storeA = store.dispatch(mountStore('a'));
this.storeB = store.dispatch(mountStore('b'));
}
componentWillUnmount() {
store.dispatch(unmountStore('a'));
store.dispatch(unmountStore('b'));
}
render() {
return (
<div>
<Provider store={this.storeA}><SomeConnectedReduxComponent /></Provider>
<Provider store={this.storeB}><SomeConnectedReduxComponent /></Provider>
</div>
);
}
} |
Ya, mounting stores seems like a very clean abstraction to me. I'm just not sure how it would work implementation-wise. It seems like you could do the mounting with I think it's also important to ensure that even though that store is mounted, the component that mounted it doesn't 'own' the state. It's still accessible to all other reducers and components. |
The mounted things aren't real stores—they're just projections with a Store-like API. In reality, they're like // lib code
getState() {
return realStore.getState().localStores[localStoreKey];
}
dispatch() {
return realStore.dispatch({
type: 'LOCAL_STORE_ACTION',
key: localStoreKey,
action: action
}
subscribe(listener) {
return realStore.subscribe(listener); // bonus: whether realStore.getState().localStores[localStoreKey] changed
} Therefore there is no need for // app code
combineReducers({
normalTodos: todos,
localStores: createLocalStoresReducer()
}) // lib code
function createLocalStoresReducer(state, action) {
switch (state) {
case CREATE_LOCAL_STORE:
return { ...state, [action.key]: action.reducer(undefined, { type: 'init' }) }
case LOCAL_STORE_ACTION:
return { ...state, [action.key]:
// lol whatever I haven't thought this through
}
} as you see I have no real idea what I'm talking about lol. But if it works with record/replay and time travel then it's done right. |
Ahh yes of course. Ya that's basically what i'm doing in redux-ephemeral, but exposing an api that looks like mounting stores is nice. I think one problem with it would be the extra Also, I think maybe the localization of the ephemeral stores that I am doing in redux-ephemeral right now might actually be a mistake. Action filtering by key should maybe be curried into the reducer if people want that, so that the ephemeral reducers may still respond to global actions. E.g. function componentWillMount () {
this.storeA = store.dispatch(mountStore('a', makeReducer('a')));
this.storeB = store.dispatch(mountStore('b', makeReducer('b')));
}
function makeReducer (key) {
return (state, action) => {
if (action.type === GLOBAL_ACTION) {
// respond locally to a global thing
}
if (action.key === key) {
switch (action.type) {
// respond locally to a local thing
}
}
return state
}
} Makes a little more work for the user, but adds a lot of flexibility I think. Especially in the realm of higher-order components that you can parameterize with the application's global action types. |
I haven't packaged this up nicely or anything but I have independent arrived at a solution that looks very similar to the code @ashaffer provided above. I've found that my "subcomponent reducers" do in fact need to respond to global actions in addition to local actions in order to get things done, and I do end up having a sort of global "public action API" that subcomponents can depend on. My solution differs from ideas discussed above in that I consider the subcomponent state private to the component in question even though it is stored in the global store. There is nothing that stops you from looking up a subcomponent's state if you know its More information: I implement runtime mounting of reducers by registering a reducer for each This The In my solution I do not support |
I realized today, and i'm not sure if you were suggesting this already @gaearon, but what you really want is for parent component's to mount their children, not for components to mount themselves. E.g. // Let's say this is a nav component
function beforeMount (props) {
// props.stateKey is an identifier that represents our current component's local state in the tree
actions.createEphemeralStore(props.stateKey, 'dropdown', Dropdown)
}
function render (props) {
return <Dropdown state={props.dropdown} />
} Then that component's parent is doing something like: function beforeMount (props) {
actions.createEphemeralStore('nav', Nav)
}
function render (props) {
return <Nav {...props.nav} />
} So dropdown itself doesn't actually create or destroy any ephemeral state. Though it could export a creator for its own state It's slightly syntactically cumbersome relative to components just automatically getting their own state, but I think it makes the couplings really explicit in kind of a nice way. Your pre-mounting function then identifies all of your sub-components that have state, and can put them wherever it deems most appropriate - and also then by definition has access to them. It's also super easy to create nice decorators for the mount/unmount hooks to make the syntax really minimal. PS: Sorry I keep using non-react hook names and syntax, I don't use it so I can never remember their names for these things when i'm writing this stuff. I hope everything is still clear. |
Alright guys, I have a working solution for vdux, and a strong primitive for redux, I think:
There is a working, though extremely non-pretty example in vdux right now. It is the 'todo' example. It should be pretty straightforward to create a variant of vdux-local on top of redux-ephemeral for whatever your preferred virtual dom framework is. |
I don't get it, why it's bad to use |
One example would be application state that gets sent to a logger when an exception is caught. If you hide transient state in the component as an implementation detail, you may not be able to reproduce the issue, since the necessary condition may only be present when you click on X while Y and Z are open but A and B are closed. |
@timaschew Partially it's just the aesthetics of truly having all state in one place. There are some practical benefits though:
Another thing to point out is that react components are not actually pure - they own their own state. Migrating state into redux allows you to have a truly pure render function, if that is important to you. |
okay, I understand if the you have at least one of the needs you listet. a component with a help icon, and if you click on that icon you want to display a help text, the visibility is toggled on each click on the help icon. I don't see here a reason to pass this to the whole redux flow |
@timaschew Ya, I think it just comes down to what is important to you. I certainly wouldn't say that it's wrong to use React's local component state. But the ultimate goal here, for me at least, is to have the primitive In your help example, that little piece of local state in your help component infects the entire tree above it. Now everything that contains help is impure, and you can no longer infer from its arguments exactly what it will return - you have to actually run it to see. The benefits of doing this are definitely a little abstract for now because the benefits of it are largely in more powerful abstraction, code clarity (ofc a matter of opinion here), tooling, and automated verification capabilities. |
Thanks for the discussion! I'm closing this as inconclusive. |
A little late to the party, but I put something together and it seems to work quite well. It needs a lot of refinement. I'm sure there's a better way to do this. I overrode |
Also made a small library adapting |
There’s been some interesting work in https://github.com/threepointone/redux-react-local. |
Thanks for the mention, Dan. I got some work into this over the weekend, and it seem to match up with your original RFC. A couple of notes -
|
I take back what I said about 'transplanting', got it to work just fine. You can now render the same component anywhere and it syncs state/behavior, just fine. A gif - for this code - https://github.com/threepointone/redux-react-local/blob/master/example/transplant.js |
server side rendering/rehydration seems to work fine ootb too, except when you need to dispatch actions/prepopulate these stores. For that, I had to introduce 2 small helpers to 1. populate a store with local reducers 2. 'sanitize' a store's state to be |
|
A new approach that might be of interest on this front is redux-fractal . |
This web component allows for separate stores per component: https://github.com/lastmjs/redux-store-element Multiple components can share the same store by name, and all actions fired from a component are scoped to the store that the component has attached itself to.
|
I'm adding this to the discussion: gcazaciuc/redux-fractal#1 It's about accessing parent component state from children. |
As @slorber notes, once you go single-treeish, local component state begins to bother you.
I don't feel strongly about it (I'm fine with some local component state here and there), but it would be interested to explore API and implementation-wise the idea of replacing React local component state with state backed by Redux with “ephemeral” reducers whose data is erased when their owner component unmounts.
I'm not sure if I'm actually making sense here.. Let's say that the litmus test is:
state
andsetState
as props.{ type: SET_LOCAL_STATE, ownerComponentId: '42424242' }
Related: rethinking React's
key
andstate
.The text was updated successfully, but these errors were encountered: