-
-
Notifications
You must be signed in to change notification settings - Fork 15.2k
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
Confusion about initial state, reducers and the Store. #1189
Comments
Please read http://stackoverflow.com/a/33791942/458193, it should answer this question. In short:
All of this should be covered by http://stackoverflow.com/a/33791942/458193. This isn't about protecting users, it's about keeping pieces encapsulated but having a way to hydrate the global state when needed. We find that the current solution strikes a good balance. |
Hey, thanks for the response! Again, I may be missing something here, but I still don't see why people shouldn't be free to decide if they want to use default arguments or not.
This feels kind of like conflating functions with objects. Objects are meant to encapsulate state (of a certain 'shape'), but functions generally just take some arguments and return something based on them, right? We like reducing and immutable values and stuff, so why not be "functional" about functions themselves too? function deeplyNestedReducer(state = this.gets.kind.of.silly, action) {
// Do something with state that's only the 'silly' part anyway.
// Now this function gets the exact same data in two ways: 1st as the default arg,
// and later on, as a piece of the 'state tree' given to the Store as initialState.
// This feels highly silly.
} If reducers didn't have to initialize their own state, then I could write: function deeplyNestedReducer(state, action) {
// No silliness in the function arguments, but we still have the exact same data.
// This time it's actually passed in by another function.
// It's like we're doing functional programming!
} What's the 'right' initial state for this reducer? function customer(state, action) {
// Maybe do something to a customer
} Obviously, we don't know what customer it will be called with. So I guess we'll initialize it with an empty object, but what exactly is the benefit in having to do that? Calling this function with no customer is going to be an error either way, so why not let everyone leave out the default argument if they feel like it? Some people want to put default arguments everywhere, and that's perfectly fine with me. Maybe they even have a good reason. Maybe they see something I don't. But I don't see why I shouldn't be allowed to not sprinkle default arguments everywhere, especially if it feels highly silly to me. |
I think you're being confused and you need to look closely at our examples. 1. Reducers don't actually receive the root state. They receive the part they care about. What you're describing is an anti-pattern: function deeplyNestedReducer(state = this.gets.kind.of.silly, action) {
// Do something with state that's only the 'silly' part anyway. Reducers don't usually receive the root state object. They receive only parts of the state relevant to them, and you compose them to get the root reducer. // state argument is *not* root state object
function counter(state = 0, action) {
// ...
}
// state argument is *not* root state object
function todos(state = [], action) {
// ...
}
// finally, root reducer delegates handling parts of its state
function app(state = {}, action) {
return {
counter: counter(state.counter, action), // only part of the state is passed
todos: todos(state.todos, action), // only part of the state is passed
};
}
// create with no initial state—useful for initialization
let store = createStore(app);
console.log(store.getState()); // { counter: 0, todos: [] }
// ... or create with some initial state—useful for hydration
store = createStore(app, { counter: 10 });
console.log(store.getState()); // { counter: 10, todos: [] } 2. You don't need to specify something twice. You're saying:
Please show in my specific example where it gets the same data in two ways. I don't see any duplication. The whole point is that reducers are autonomous, their parent reducer doesn't know their data type and delegates handling of the subtrees to child reducers, and there is a way to hydrate the state. I'm happy to continue this discussion but let's use a specific example as a basis instead of pseudocode because it's very hard to understand where exactly the confusion lies. |
This is exactly what happens. They'll receive that state as |
There's a reason why I put the word 'confusion' in the topic! :) ( I'm just getting started with Redux and other stuff, so I don't have any real code to show you )
We're actually in agreement here. In a comment string, I said the reducer would only receive "the 'silly' part". What I meant with that was basically what you're describing. Some other, combined reducer would hand it the part of the state tree it's supposed to handle. The default argument "this.gets.kind.of.silly" described a hierarchy of named objects, and the last part was the 'silly' part :) What I meant with a reducer getting the same data "in two ways" was once as the default argument, and once again from its parent reducer when the actual data (given as initialState to the Store) is being handled. But what am I missing? If I have a 'state tree' with multiple nested levels of data, and specific nested reducers are supposed to handle specific nested parts of it, what is the right way to give them their initial state/data? You see, I've got a bunch of data, and a bunch of functions. The structure of the reducers corresponds to the structure of the data. As far as I can tell, my problem is that I can't just go straight to having the reducers handle the state tree for real, without the 'intermediate step' of the library handing Imagine a server-side application with lots of functions calling other functions, and let's say most of them can't actually do anything without getting the kind of 'real arguments' they're supposed to work on. Wouldn't it be kind of silly to intentionally pass Besides, even the funcs that prepare for getting nothing won't just spring some data into existence and work on that instead. Maybe a part of my/the (:p) problem here is that 'state' kind of gets conflated with 'data'? Something like.. It may well make sense for 'empty state' to 'spring up' from the reducers, but the same doesn't apply to persistent or 'actual' data. function customer(state, action) {
// A customer would obviously be 'actual data'.. there's no sensible default for it.
} What does it mean for that reducer to initialize itself with an empty object? It's supposed to get a customer, and it can't do anything without one. Not only that, but the empty object it initializes for itself has no use outside of this reducer either. |
Create a JSBin fiddle demonstrating your confusion and share it. I imagine it wouldn't be more than 30 lines, would it?
What I don't understand is why you used default argument syntax for that. Yes, it will receive the part relevant to it, but you would never need to write something like
Normally you let each reducer specify its initial data. However you already have that data ready for hydration (from localStorage, from the server state in a server-rendered application), you pass it to
Sorry, I can't continue the discussion in this manner. It is very theoretical. What you're talking about is obvious to you, but unless you show some (even contrived) example code, I can't help you because I don't understand you. On my side, I have shown the example, and we have plenty in the docs and the repo as well, so it's your turn.
Yes, perhaps!
Yes there is a sensible default, if you have a customer creation form. For example: function customer(state = { isPublished: false, name: 'Jane Doe' }, action) {
} When you don't have such a form just don't specify it. Nobody forces you to specify the initial state for the reducers that are called on-demand by other reducers. function customer(state, action) {
} We only force you to specify the reducer's initial state as default argument when you put a reducer inside |
See also how we can create reducer factories that specify the initial state. It wouldn't be encapsulated if I forced the consumer to always remember to specify the exact state shape as an argument to So, to clarify again, this is an anti-pattern: const store = createStore(rootReducer, {
customersById: {},
customersPagination: {
isFetching: false,
ids: []
}
}); Specifying an object literal as the initial state when creating a store is an anti-pattern. When you do it this way, any time you change a reducer you also must remember to change the initial state shape. Reducers are no longer encapsulated. So why does the second argument even exist? For hydration of pre-generated state tree. This is fine: // warning: pseudo code, no error handling and perf
const savedState = JSON.parse(localStorage.getItem('saved-store-state'));
const store = createStore(reducer, savedState);
store.subscribe(() => {
localStorage.setItem('saved-store-state', JSON.stringify(store.getState()));
}); This is also fine: // warning: pseudo code, might have security problems, won't actually work as is
// server
const store = createStore(rootReducer);
store.dispatch(fetchData()).then(() =>
response.send(`
<html>
<body>
${React.renderToString(<Provider store={store}><App /></Provider>))}
<script>window.SERVER_STATE = ${JSON.stringify(store.getState())}</script>
<script src='/static/app.js'></script>
</body>
</html>
`)
);
// client
const store = createStore(rootReducer, window.SERVER_STATE); That's what the second argument is for. Hydrating the existing state. However we never need to repeat the same initial state twice as object literal—reducers always take care of initializing the state they manage. We only use the second argument to hydrate the state that was previously generated by reducers—whether the last time the app ran, or when it was rendered on the server. |
Hmm, alright, thanks for going through so much effort in explaining stuff. I must be doing something wrong because I got very confused when writing the following example code: const data = {
stuff: {
innerStuff: {
something: ["value"]
}
}
};
const stuff = (state, action) => {
console.log("Stuff: " + JSON.stringify(state));
return {
innerStuff: innerStuff(state.innerStuff, action)
};
};
const innerStuff = (state, action) => {
console.log("Inner Stuff: " + JSON.stringify(state));
return {
something: something(state.something, action)
}
};
const something = (state, action) => {
console.log("Something: " + JSON.stringify(state));
return state;
};
const rootReducer = combineReducers({
stuff: combineReducers({
innerStuff: combineReducers({
something
})
})
});
const testStore = Redux.createStore(rootReducer);
testStore.dispatch({type: "TEST"}); Anyway, now that the
It says if the reducer gets But if I change it to: const something = (state = data.stuff.innerStuff.something, action) => {
console.log("Something: " + JSON.stringify(state));
return state;
}; .. The error goes away, as you might expect. That's certainly very ugly, and obviously not something I should be doing, so here I am, wondering if there's another way. Anyway, I think I just realized that this: const rootReducer = combineReducers({
stuff: combineReducers({
innerStuff: combineReducers({
something
})
})
}); .. results in the other reducers not actually getting called. The innermost reducer was called strangely many times, though. So yeah.. :D Confusion. Here's another try: const anotherRootReducer = (state = {}, action) => {
return {
stuff: stuff(state.stuff, action)
}
}; With that, and default arguments for the other reducers, things behave more like I'd expect! But looking back at your posts, I found this:
But that's what I'd call protecting me from myself! I'm perfectly fine with getting an exception if I've made a mistake! It's my fault, and I should be more careful! :) That's a lot like falling on a bicycle. Falling down sucks, but that doesn't mean someone should force you to use training wheels to prevent you from falling :) You just learn to be more careful. To be fair, I'm starting to feel like initial state in reducers might be alright after all. Your example of 'reducer factories' looked reasonable and all.
If a programmer chooses not to specify initial state for each of his reducers, and as a result has to give the exact right shape of data to createStore, who's forcing anyone to do anything? But if I can't not specify initial state, that sounds more like a match.
I don't get this.. how would you define 'an implementation detail' in this case? -Something that things outside of the reducer don't have to worry about? .. But it's not like a reducer can just produce whatever shape of data it wants to, right? Won't the state be used by something in a specific way, with specific expectations on its shape? |
I'm happy to continue this discussion when you show the real code you're struggling with. |
In particular this example is very confusing: const data = {
stuff: {
innerStuff: {
something: ["value"]
}
}
};
const stuff = (state, action) => {
console.log("Stuff: " + JSON.stringify(state));
return {
innerStuff: innerStuff(state.innerStuff, action)
};
};
const innerStuff = (state, action) => {
console.log("Inner Stuff: " + JSON.stringify(state));
return {
something: something(state.something, action)
}
};
const something = (state, action) => {
console.log("Something: " + JSON.stringify(state));
return state;
};
const rootReducer = combineReducers({
stuff: combineReducers({
innerStuff: combineReducers({
something
})
})
});
const testStore = Redux.createStore(rootReducer);
testStore.dispatch({type: "TEST"}); What is The example code itself seems wrong too, even though it's very hard to tell what you want to do because of const data = {
stuff: {
innerStuff: {
something: ["value"]
}
}
};
const something = (state, action) => {
console.log("Something: " + JSON.stringify(state));
return state;
};
const rootReducer = combineReducers({
stuff: combineReducers({
innerStuff: combineReducers({
something
})
})
});
const testStore = Redux.createStore(rootReducer);
testStore.dispatch({type: "TEST"}); This is why other reducers are not called—you are not using them! But again, because of the So far, I see that you might have confusion about how From your comment I assumed you watch Egghead videos, but I suggest you to give them another try. In particular these three lessons explain what
Unfortunately I'm not ready to continue this discussion without an example showing what you are trying to do with more descriptive names and data structures than |
Yeah, it's confusing. But as I believe I mentioned, I don't have any real application code with Redux yet. So basically what I'm struggling with is starting to use Redux, hopefully in a sane way that also happens to be comfortable for me personally.
I'd imagine it would be some persistent application data from the server (and database), which I'd use to "initialize" Redux when the page loads. Then I suppose it would be modified through Redux, and saved to the server as appropriate, etc.
Yeah, I was trying to make it equivalent, hoping to avoid any potential problems that I might have caused by using Anyhow, I'm open to the idea that I'm Doin' It Wrong :P and it's understandable that you'd need to see a real-world example of code to be able to see what's wrong etc. But I believe you could address the latter part of my post, since that's not related to any specific code anymore:
-What's your take on that, and the rest? Obviously, I have a problem if I don't specify default arguments, without giving the Store any initial state either.. But I've been feeling like it's also a problem if I can't not specify default arguments for reducers when I do give initial state to the Store. In this case, reducers won't be called with That's what this whole thread was originally about. I may well be using reducers in a silly way, but I don't see how that's related to having the option of not specifying default arguments for reducers when you feel like they're unnecessary (e.g. when reducers won't end up with |
I think we're finally getting to the root of this issue.
Your argument presumes that on the server you would assemble a state tree by hand to pass to the client. However this is not at all what we suggest. It is still error-prone to assemble that tree by hand. Instead we suggest to create a store object on the server, fire async actions, and let reducers handle them like usual. Those reducers that handle relevant parts will fill the relevant state according to those initial actions. Then you would just pass that state to the client. No need to create a state tree by hand. This is why we make an opinionated choice in You are completely free to opt out of this behavior by not using Finally you asked what I mean by encapsulation of state shape in reducers. Sure, in many examples components rely on state shape but that's because not all our examples are good enough. We will fix them. In the meantime please look at |
@jugimaster It is often easier to figure out a new Time to start coding a real app. On 1 January 2016 at 19:08, Dan Abramov [email protected] wrote:
|
I'm trying to keep this brief, so I may come off as more argumentative than I actually am, but here goes.. :)
I don't know that allowing people to choose whether to define default arguments for their reducers would necessarily result in fragility. How do you know? Besides, the future of front-end data handling seems to involve something like Relay/GraphQL and Falcor etc, where data is queried and received in a hierarchical way, which seems like a good fit for (potentially deeply) nested reducers too. When using Relay/GraphQL, the shape for your data is largely defined by the queries themselves. Then you'd adjust your reducers to match it. In your model, it seems the reducers define the shape of the data with their default arguments. Obviously both can't be the authoritative definition for the data's shape at the same time, and they need to be kept in sync anyway, so I don't see why people shouldn't be free to leave the default arguments out if they want to, while still retaining the benefit of reduced boilerplate that If you're using Relay, then leaving out the default arguments for your reducers could be seen as being explicit about having the shape of your client-side data defined by the queries specifically, which you're kind of supposed to do when using Relay. But now you can't do that, while using
Speaking of presumptions, I'm actually not using Node on the server! :) Neither are a lot of other people who will still have to do front-end development too, and who'd presumably(!) want to enjoy using Redux while at it! I've recently been ranting about the strange state of front-end development, now that it's somehow completely dependent on Node-based tools. We're writing JavaScript to be run on the client-side, but first we use ~'server-side' tools to "build" it, just because "we" insist on using language features that aren't widely enough supported by browsers yet.
Alright, but it seems like now the 'unit of encapsulation' is a file, instead of a reducer function. Files certainly contain 'state' / data / 'shape' etc, but functions generally don't. Anyway, I don't think that results in the state being 'an implementation detail' of the functions.
Yeah, that seems like a good idea. But please let people make mistakes and learn from them. It might even result in discovering a new way of doing things that's even better than yours! :) |
Oh, and I generally detest the idea of 'forcing' people to do something 'for their own good'. Here's a brief example:
But despite this kind of mentality being widespread in a lot of different ways, no one would be happy about a doctor personally forcing them to eat carrots or exercise. In that case, the coercion would be seen as immoral. But coercion is just coercion, even when it's being applied as a means towards ostensibly good ends. |
It's my guess as a library author that comes from my experience building a complex client-side app, as well as from the issues and questions I've been answering here before for the past several months.
This library is not Relay. The vast majority of Redux users doesn't use Relay. People who use Relay tend to switch to it fully, or use Relay for data entities while only keeping Redux for the local state. I agree declarative data fetching is the future—but I just don't see your pain point. I can't see the connection between having
That's not what I'm presuming. I'm trying to support a very popular use case which used to be hard to implement with traditional Flux. I understand not everybody uses Node on the server (in fact I don't, as I use Python). It's the first time you mentioned that you don't use Node in the whole issue. It would've helped if you said it before because I have a very specific answer for this use case:
I hope this helps.
I feel for you but how is this even remotely related to Redux? You can write Redux code in ES5, use a global
Thanks. This is exactly what I try to do. However I learn from issues and questions people post when working on real apps so I can understand their real problem better from the code, rather than the problems they perceive. These warnings were gradually refined through several releases based on how people used Redux and the issues, misunderstandings and real problems they had. I'm happy to come back to this discussion after you've built an app with Redux and have some real code to share and discuss.
I'm afraid this thread is going off the rails. I am closing it, as I believe I justified my choices and offered workarounds:
I also offered you a few suggestions:
I hope this helps. I thank you for your time, and I hope that you try to build something with Redux and let us know your experiences, as well as the pain points you accumulated after you've got something working. |
That library is "forcing" you to do something is a strange way to view open source. We are talking about 10 line utility function. As a library we made a choice to add this warning because adding |
I'm still new to Redux, so I might be missing something here.
When a reducer doesn't recognize an action, it's supposed/idiomatic to return the
state
given to it, right?But when Redux is initializing the reducers, it calls them with
undefined
, and in that situation it's an error to return the givenstate
.So if the Store is given initial data, wouldn't it make sense for Redux to 'initialize' the reducers with that instead? That way we wouldn't have to worry about setting up some kind of initial state for each reducer directly.
It seems to me that Redux is trying to protect its users from themselves, and I can't help but think that's a bad idea. It's a bit like having a bicycle with training wheels you can't remove :)
The text was updated successfully, but these errors were encountered: