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

State properties without reducers. #1457

Closed
chyzwar opened this issue Feb 28, 2016 · 13 comments
Closed

State properties without reducers. #1457

chyzwar opened this issue Feb 28, 2016 · 13 comments

Comments

@chyzwar
Copy link

chyzwar commented Feb 28, 2016

It is possible to have reducers that do not have defined actions for some state properties. This will be the case for single reducer working on root state. combineReducers need define a reducer for each property on state otherwise it will drop keys on initalState.

Since normal reducer do not need to address changes in all state properties, why combineReducers and createStore are such strongly opinionated??

In many cases, state will have properties that will be read once and reducer is not really needed. For example server side rendering where component will be rendered only once and props data will be taken from connected store.

I would like to avoid defining "empty" reducers like state => state just to avoiding keys in my state object being dropped. I still want to have some reducers because sometimes I would like to two components to talk to each other during render.

@gaearon
Copy link
Contributor

gaearon commented Feb 28, 2016

Can you show some code, both for client and server side? I have a hard time imagining how you’re using combineReducers() and “constant” state and the relevant code snippets would really help!

@chyzwar
Copy link
Author

chyzwar commented Feb 28, 2016

I plan to build isomorphic app. I use components represents pages in my app. It will be not a SPA in normal sense.

import React from 'react';
import { connect } from 'react-redux';

import Head from 'components/Head';
import Meta from 'components/Meta';
import Body from 'components/Body';

import UniversalAnalytics from 'components/UniversalAnalytics';
import PageInsights from 'components/PageInsights';
import JavaScript from 'components/JavaScript';
import StyleScheet from 'components/StyleScheet';

class MainPage extends React.Component {
  render() {
    const { meta, styles, scripts, insights } = this.props;
    const { analytics: { google, facebook } } = this.props;

    return (
      <html>
        <Head>
          <Meta meta={meta} />
          <StyleScheet styles={styles} />
          <PageInsights insights={insights} />
        </Head>

        <Body>
          <span> Main page there</span>

          <UniversalAnalytics trackingCode={google.trackingCode} />
          <JavaScript scripts={scripts} />
        </Body>
      </html>
    );
  }
}

function select(state) {
  return state;
}

/**
 * Redux connected main page
 */
export default connect(select)(MainPage);

This will render almost full html, I dont need reducers for analytics, styles, meta etc...

import React from 'react';
import ReactDOMServer from 'react-dom/server';
import { Provider } from 'react-redux';
import { createStore } from 'redux';

import buildStore from './buildStore';

export default class ReactRenderer {
  constructor() {
    this.type = 'react';
  }

  render(Page, storeFunction, reducer) {
    const promise = buildStore(
        storeFunction()
      );

    return promise
      .then((initState) => {
        const pageStore = createStore(reducer, initState);
        const page = ReactDOMServer.renderToString(
          <Provider store={pageStore}>
            <Page/>
          </Provider>
        );

        return `<!doctype html> ${page}`;
      });
  }
}

This will render a page based on initalStore, Page composite component like MainPage and server side reducers. First It will use storeFunction that return object with mixed values and promises. buildStore will resolve all promises. Create store will take reducers and initalState and connect this to page component.

import { combineReducers } from 'redux';

import analytics from 'redux/reducers/analytics';
import posts from 'redux/reducers/posts';
import auth from 'redux/reducers/auth';

const reducers = {
  analytics,
  posts,
  auth,
};

export default combineReducers(reducers);

I will have reducers for server side only for a things that can affect my render, but I dont need to have reducers for user interactions.

@chyzwar
Copy link
Author

chyzwar commented Feb 28, 2016

In my cases initialState will be different depends on what route in server side user will hit.
reat-router and redux-sage will take over after initial load. Shape of state will change but I hope to follow redux principles.

In my case I will need to create custom implementation of combineStores or add empty reducers for all possible state properties.

@gaearon
Copy link
Contributor

gaearon commented Feb 28, 2016

I will have reducers for server side only for a things that can affect my render, but I dont need to have reducers for user interactions.

combineReducers() is designed around the assumption that you want to use the same reducers on client and server. Since your design is a bit unusual, I think it’s fair that you need to implement your own root reducer like

const reducer = (state = {}, action) {
  return Object.assign({}, state, {
    posts: posts(state.posts, action),
    // other client side reducers
  })
} 

which would not drop the keys and would have no warnings.

@melnikov-s
Copy link

To piggyback off this issue, I too experienced some frustration with the sometimes over stringent verifications in combineReducers. I would add some state which at the time is read only (does not need modifications to any actions) something like the currently logged in user. Only to see it fail because I did not add an identity-like reducer let user = (state = {}) => state.

Another minor annoyance is using parameter destructuring with flux standard actions. So I would attempt to write something like:

function myReducer(state = {}, {type, payload: {propA, propB}})

but that will also fail because of combineReducers sanity checks.

I forced to do

function myReducer(state = {}, {type, payload: {propA, propB} = {}})

instead.

@gaearon
Copy link
Contributor

gaearon commented Mar 2, 2016

Please help me understand what you are doing better.

I would add some state which at the time is read only (does not need modifications to any actions) something like the currently logged in user

How do you “add” this read-only state?

@melnikov-s
Copy link

Maybe I threw you off with the phrase "read-only". Its just node in the state tree that does not respond to any action (but it may in future versions of the application), therefore its not modified throughout the applications life-cycle. Its added as part of the initial state when creating a store.

@gaearon
Copy link
Contributor

gaearon commented Mar 2, 2016

Its added as part of the initial state when creating a store.

Please show how you create the store. initialState argument is not meant to be filled out directly. It is meant to be specified only when it is persisted or hydrated from the server.

@melnikov-s
Copy link

I'm not quite clear as to what you mean, we create the store in what I thought was the standard way.

let store = createStoreWithMiddleware(reducers, initialState)

where initialState comes directly from the server. We don't use server side rendering if that helps any.
Within initialState there are some properties that are never changed in response to an action but that still require a reducer in order to comply with combineReducers initial verification.

Would you suggest that those type of "read-only" properties do not belong in a redux store?

@gaearon
Copy link
Contributor

gaearon commented Mar 2, 2016

Would you suggest that those type of "read-only" properties do not belong in a redux store?

I’d say so. If these are constants, just make a module that exports these constants and lets you inject their values once from the server payload.

An alternative I suggest is to express this initial data as actions. For example your server may emit something like

window.INITIAL_ACTIONS = [
  { type: "AUTH_SUCCESS", userId: 3 },
  { type: "PRELOAD_DATA", { ... } },
  // could have more actions
]

In your index.js, dispatch those actions right after creating the store:

window.INITIAL_ACTIONS.forEach(store.dispatch)

Now you can write reducers that don’t care whether those actions are local or not.

function auth(state = {}, action) {
  switch (action.type) {
  case AUTH_USER:
    return { ...state, userId: action.userId }
  default:
    return state
  }
}

The benefit of this approach is that it’s easier to later perform these actions on the client as well, if there is ever a need for this, and that it is very easy to preload the data (e.g. pre-fetched responses for entities that are likely to be requested) in a uniform way without coupling the server code to the state shape of the client code. Everything goes as an action.

I hope this helps!

@gaearon gaearon closed this as completed Mar 2, 2016
@melnikov-s
Copy link

Thanks @gaearon , that helps.

@e1himself
Copy link

e1himself commented Nov 16, 2017

Hi here,

I came here because I'm having a discussion with another dev about keeping never-changing constants inside redux store (api access token, or some settings).

So the guy is advocating the approach of Single Source of Truth, and thus keep the constants inside the store too. Similar to this StackOverflow answer: https://stackoverflow.com/a/45227593

const rootReducer = combineReducers({
 ...
 settings: (state = {}) => state,
 ...
});
window.__INITIAL_STATE__ = {
 ...
 settings: { ... },
 ...
};

While I think of redux store as a container for changing data. If something is never changed during the page JS life (it's still different for every user), I prefer to pass it as a prop into the app component from outside (a PHP template injecting props values fetched from database). I feel like storing constants is abusing the store purpose.

I really hope to hear @gaearon thoughts on this. Thanks.

@joaovieira
Copy link

I have another use case that causes the same problem.

I've got dynamic chunks per route that add dynamic reducers. These dynamic chunks are created via Webpack and are automatically and asynchronously loaded on route change (or page load). Asynchronously means that, even on page load, the dynamic chunk will always be fetched after the initial chunk (the one that creates the store) is executed.

The server is pre-hydrating the store for that dynamic reducer, yet, the reducer wasn't loaded at the time the store and initialState were created.

I.e. in chronological order

  1. page load
  2. server sends page with hydrated store (in html attributes)
  3. browser fetches initial chunk
  4. in parallel:
    4a. dynamic chunk for route is fetched
    4b. initial chunk executes and creates store with initialState. This throws the warning Unexpected key "{dynamicNamespace}" found in previous state received by the reducer.
  5. the dynamic reducer that handles {dynamicNamespace} is added immediately after

Seems like a valid flow, but that warning is annoying me. I don't want to add dummy reducers just to silent the warning as my entry chunk should be completely agnostic of other modules.

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

No branches or pull requests

5 participants