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

Confusion about initial state, reducers and the Store. #1189

Closed
jugimaster opened this issue Dec 30, 2015 · 17 comments
Closed

Confusion about initial state, reducers and the Store. #1189

jugimaster opened this issue Dec 30, 2015 · 17 comments

Comments

@jugimaster
Copy link

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 given state.

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

@gaearon
Copy link
Contributor

gaearon commented Dec 30, 2015

Please read http://stackoverflow.com/a/33791942/458193, it should answer this question. In short:

  • Reducers specify their own initial state shape because they should be in control of it. State shape often changes, and you shouldn't have to remember to modify it somewhere else. Additionally some reducer composition patterns actually mean you don't have the initial state until some nested data is initialized (for example, a new entry by dynamic key). In this case you can't supply it beforehand anyway.
  • You can pass the whole initial state tree as a second argument when creating the store. This is useful for hydrating persisted data or data received from the server. However we never recommend building that state tree in your code. The reason is the same: to keep reducers self-sufficient and reusable.
  • This means when your reducers specify initial state and you also specify the initial state tree when creating the store, the state tree wins, just by the virtue of following the rules you outlined above. This lets you prefill the saved state for some reducers, and let others calculate their own initial state. This is a powerful pattern for universal apps that sometimes have initial state from the server and sometimes don't.

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.

@gaearon gaearon closed this as completed Dec 30, 2015
@jugimaster
Copy link
Author

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.

Reducers specify their own initial state shape because they should be in control of it.

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.

@gaearon
Copy link
Contributor

gaearon commented Jan 1, 2016

I think you're being confused and you need to look closely at our examples.
There are two points of confusion I can see:

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:

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

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.

@gaearon
Copy link
Contributor

gaearon commented Jan 1, 2016

So if the Store is given initial data, wouldn't it make sense for Redux to 'initialize' the reducers with that instead?

This is exactly what happens. They'll receive that state as state argument. In this case the optional parameter won't matter at all. Optional arguments are only used when the value is undefined. This is why I'm asking for runnable code example: it seems that we are talking past each other because of some misconception about how these pieces fit together.

@jugimaster
Copy link
Author

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 )

Reducers don't actually receive the root state. They receive the part they care about

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 undefined to them.

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 null to them? Of course some functions need to be prepared not to explode when given null as an argument, but in some cases, you can guarantee it will never happen and then you don't need to worry about it.

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.

@gaearon
Copy link
Contributor

gaearon commented Jan 1, 2016

( I'm just getting started with Redux and other stuff, so I don't have any real code to show you )

Create a JSBin fiddle demonstrating your confusion and share it. I imagine it wouldn't be more than 30 lines, would it?

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 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 state = topLevelState.bla.foo.bar to retrieve it. You'd just receive it as the state argument from the parent reducer.

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?

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 createStore, and it will be used instead. This is akin to "resuming" an application from a given "start" state, rather than letting it "boot".

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 undefined to them.

Wouldn't it be kind of silly to intentionally pass null to them? Of course some functions need to be prepared not to explode when given null as an argument, but in some cases, you can guarantee it will never happen and then you don't need to worry about it.

Besides, even the funcs that prepare for getting nothing won't just spring some data into existence and work on that instead.

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.

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.

Yes, perhaps!

function customer(state, action) {
  // A customer would obviously be 'actual data'.. there's no sensible default for it.
}

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 combineReducers() because we don't want to throw if the store was created with undefined initial state. The client needs to be able to "boot up" without any data—at least it is the requirement we're enforcing because that's what apps need most of the time. I think you'll want your app to be able to fetch data for itself even if it was booted without pre-loaded data.

@gaearon
Copy link
Contributor

gaearon commented Jan 1, 2016

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 createStore because the state shape of that reducer is its implementation detail.

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.

@jugimaster
Copy link
Author

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 something -reducer has no initial state, I get this error:

Reducer "something" returned undefined during initialization. If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined.

It says if the reducer gets undefined, then it "must explicitly return 'the' initial state".. but I didn't want to return any! :) I guess my problem boils down to getting that error.

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:

We only force you to specify the reducer's initial state as default argument when you put a reducer inside combineReducers() because we don't want to throw if the store was created with undefined initial state.

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.

It wouldn't be encapsulated if I forced the consumer to always remember to specify the exact state shape as an argument to createStore [..]

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.

[..] because the state shape of that reducer is its implementation detail

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?

@gaearon
Copy link
Contributor

gaearon commented Jan 1, 2016

I'm happy to continue this discussion when you show the real code you're struggling with. stuff, innerStuff, TEST obscure the nature of your problem too much, and I can't understand the real context of your issue and what you're trying to achieve. You may not think that it matters, but it really does.

@gaearon
Copy link
Contributor

gaearon commented Jan 1, 2016

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 data? Why do you have it beforehand? Is it some pre-loaded data available at the application start? Where is it coming from? Does it always exist?

The example code itself seems wrong too, even though it's very hard to tell what you want to do because of stuff, innerStuff, etc. You are creating stuff and innerStuff reducer functions but you are not actually using them (check your code, they are unused variables). The only function you're actually using in your code is something so you might as well cut your code:

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 stuff, innerStuff I can't help you because I don't understand what you were really trying to do.

So far, I see that you might have confusion about how combineReducers() works. It actually generates a reducer. So it's completely redundant to write innerStuff reducer if you already generated it with combineReducers({ something }). The returned function is equivalent to what you wrote in innerStuff, and you're not passing hand-written innerStuff anywhere so it gets ignored.

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 combineReducers() is, and how exactly it works:

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

@jugimaster
Copy link
Author

I'm happy to continue this discussion when you show the real code you're struggling with. stuff, innerStuff, TEST obscure the nature of your problem too much, and I can't understand the real context of your issue and what you're trying to achieve.

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.

What is data? Why do you have it beforehand? Is it some pre-loaded data available at the application start? Where is it coming from? Does it always exist?

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.

The returned function is equivalent to what you wrote in innerStuff

Yeah, I was trying to make it equivalent, hoping to avoid any potential problems that I might have caused by using combineReducers wrong somehow.

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:

We only force you to specify the reducer's initial state as default argument when you put a reducer inside combineReducers() because we don't want to throw if the store was created with undefined initial state.

But that's what I'd call protecting me from myself!

-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 undefined when actually handling the actual state (or data, as the case may be). They're called with it only by Redux itself, when "asserting the sanity" of the reducers.

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 undefined data on their hands because of anything I did!).

@gaearon
Copy link
Contributor

gaearon commented Jan 2, 2016

I think we're finally getting to the root of this issue.

combineReducers() assumes that you want your code to run fine without any initial state passed to the store as the second argument. Otherwise your app becomes fragile. What if some particular key isn't specified in some cases (for example when the user is logged out)? It's hard to remember to always fill all possible state tree keys because the client relies on a specific prepared tree on the server. Moreover different pages will need different data so you can't prepare all of it. And undoubtedly people will open pages dynamically at which point you want to load more data.. So in addition to data you'll have to prepare empty "placeholders" for every possible state field because otherwise your app will crash! Add to this the fact that there are multiple possible initial states (logged in user, anonymous user, etc), and it's very hard to make sure you don't crash in some weird case.

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 combineReducers() that your app should be able to boot without any externally supplied predetermined state tree. It should be able to initialize and slowly accumulate the data as it becomes available, regardless of the initial state. That's the decision we made for reasons above.

You are completely free to opt out of this behavior by not using combineReducers(). The checks live there. You are free to write your own root reducer by hand and manage the state in any way you like, if you don't agree with our opinion expressed in the design of combineReducers().

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 shopping-cart example. There, we define "selectors" alongside reducers in the same file. This is how you keep the state shape an implementation detail. The only code relying on that state shape would be in that file, and it would be safe to change without changing anything else.

@johnsoftek
Copy link
Contributor

@jugimaster It is often easier to figure out a new
function/library/framework by implementing something real rather than
abstract. Dan has been amazingly patient in providing guidance to you and
pointing out the many examples that he has devoted his time to preparing.

Time to start coding a real app.

On 1 January 2016 at 19:08, Dan Abramov notifications@github.com wrote:

I think we're finally getting to the root of this issue.

combineReducers() assumes that you want your code to run fine without any
initial state passed to the store as the second argument. Otherwise your
app becomes fragile. What if some particular key isn't specified in some
cases (for example when the user is logged out)? It's hard to remember to
always fill all possible state tree keys because the client relies on a
specific prepared tree on the server. Moreover different pages will need
different data so you can't prepare all of it. And undoubtedly people will
open pages dynamically at which point you want to load more data.. So in
addition to data you'll have to prepare empty "placeholders" for every
possible state field because otherwise your app will crash! Add to this the
fact that there are multiple possible initial states (logged in user,
anonymous user, etc), and it's very hard to make sure you don't crash in
some weird case.

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 combineReducers() that your
app should be able to boot without any externally supplied predetermined
state tree. It should be able to initialize and slowly accumulate the data
as it becomes available, regardless of the initial state. That's the
decision we made for reasons above.

You are completely free to opt out of this behavior by not using
combineReducers(). The checks live there. You are free to write your own
root reducer by hand and manage the state in any way you like, if you don't
agree with our opinion expressed in the design of combineReducers().

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 shopping-cart example. There, we define "selectors"
alongside reducers in the same file. This is how you keep the state shape
an implementation detail. The only code relying on that state shape would
be in that file, and it would be safe to change without changing anything
else.


Reply to this email directly or view it on GitHub
#1189 (comment).

@jugimaster
Copy link
Author

I'm trying to keep this brief, so I may come off as more argumentative than I actually am, but here goes.. :)

combineReducers() assumes that you want your code to run fine without any initial state passed to the store as the second argument. Otherwise your app becomes fragile. What if some particular key isn't specified in some cases (for example when the user is logged out)?

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 combineReducers gives you.

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 combineReducers, and that seems a bit problematic to me.

Instead we suggest to create a store object on the server, fire async actions, and let reducers handle them like usual.

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.

In the meantime please look at shopping-cart example. There, we define "selectors" alongside reducers in the same file. This is how you keep the state shape an implementation detail.

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.

The only code relying on that state shape would be in that file, and it would be safe to change without changing anything else.

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

@jugimaster
Copy link
Author

Oh, and I generally detest the idea of 'forcing' people to do something 'for their own good'.

Here's a brief example:

  • Carrots are good for you.
  • Therefore, not eating carrots is bad for you (relative to eating them).
  • Therefore, everyone should be eating carrots.
  • A lot of people might not eat carrots even though they should.
  • Therefore, "we" need to protect them from themselves.
  • Therefore, the government should criminalize not eating carrots, for everyone's own good!
  • We are great moral people because we are forcing people to do what's good for them.

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.

@gaearon
Copy link
Contributor

gaearon commented Jan 2, 2016

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?

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.

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.

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 combineReducers gives you.

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.

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 undefined as the initial state of some reducers, and using Falcor or Relay. (I've read the rest of your message and I still don't understand it. Maybe because you don't offer any specific examples :-) Somehow reducer in redux-falcor project starts with initial state of {} and later updates that object, and it's not a problem for them. I've also never heard about this problem from people who integrated Redux and Relay before.

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!

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:

If you for some reason don’t use Redux on server but you want to prefill the data anyway (e.g. maybe you use something other than Node), the approach with dispatching actions is your next best bet because it doesn’t require your backend to know the state shape. In this case, you should pass those actions as JSON to the client, and dispatch() all of them right after the store is created.

I hope this helps.

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.

I feel for you but how is this even remotely related to Redux? You can write Redux code in ES5, use a global window.Redux variable, and be happy. This issue is starting to go off the rails.

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

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.

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.

I'm afraid this thread is going off the rails. I am closing it, as I believe I justified my choices and offered workarounds:

  • Write your own combineReducers()
  • Write (state = {}, action) or (state = null, action) when you don't know the state shape

I also offered you a few suggestions:

  • Let reducers manage their own shape, define selectors alongside them to keep the state shape implementation detail
  • Instead of prepopularing the state tree from non-Node backend, prepopulate a list of actions and pass them to the client so the server stays ignorant of the state shape

I hope this helps.
This discussion is taking a lot of my time, and it's too theoretical for me to take lessons from it.
I'm closing because it has largely devolved into philosophical debate and rants about unrelated topics.

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.

@reduxjs reduxjs locked and limited conversation to collaborators Jan 2, 2016
@gaearon
Copy link
Contributor

gaearon commented Jan 2, 2016

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 = {} or = null when you mean it won't hurt you. However allowing undefined to be returned from reducers caused myriad of issues when people forgot to add a default case to their switch statements, and thus reset their state on every unknown action because of implicit return. As a library we make choices, and if you don't agree with them please feel free to copy the parts that you like and make your own library and helpers. Without sanity checks Redux is 99 lines so it shouldn't be too difficult: https://gist.github.com/gaearon/ffd88b0e4f00b22c3159

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

No branches or pull requests

3 participants