reduxjs / redux

A JS library for predictable global state management

Home Page:https://redux.js.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Graphical Cheat Sheet

uanders opened this issue · comments

I have put together a graphical cheat sheet for Redux. I am happy to PR it to Redux should there be interest. I believe it will help people to find their way through the concept & workflow.

I am also going to publish it shortly, probably on Medium with an accompanying article. Should you find any errors, please let me know. Thanks.

react-redux-workflow-graphical-cheat-sheet-v100

Looks cool. Do you have that as a "source" file of some sort? E.g., LaTex or SVG

There's also the "diagrams" thread over in #653 . Would be nice if we could fit a couple of those in somewhere useful (particularly the neat animated gifs from the "Twitter Hype to Production" slideshow).

@uanders Very nice!

  • Typo "implicitely" -> "implicitly".
  • Root store={store} should be called Provider (if nothing changed since I learned react-redux)
  • "a new state triggers re-render" should point to the connected components, they subscribe() to the store object that is passed down from Provider via React context and provide new props to their wrapped components.

...committing the comments for now.

  • <SmartComponent01> /* connect() copied to <SmartComponent01Connected> */connect() does not copy, it creates and returns a new component (which is SmartComponent01Connected) that uses (instantiates) the original SmartComponent01.
  • In SmartComponent01 typo "Mored Methods" -> "More Methods".
  • The overall terminology has changed from "smart"/"dumb" to "connected"/"presentational" (please update me if I'm wrong here)
  • In DumbComponent01 it's better to name the "dispatch" prop "onClick" to align with the built-in components.
  • dispatch() calls rootReducer() with specified actionCreator — wrong, the actionCreator gets called before the dispatch in the user-land code (in the dispatchOnClick). It's the created action object that is returned from actionCreator and gets passed to dispatch. Not obvious because written in one line:
dispatch(actionC(...));
// is the same as
const action = actionC(...);
dispatch(action);
  • In legend: no usage of react-router and immutable, suggest removing to avoid confusion.
  • In legend: reducer() updates state and state slices in store — reducer itself does not (and must not) update anything, it has to be a pure function that returns a new state given the current state and an action; it's redux store implementation that calls the reducer and updates the internally stored state during the dispatch; slicing the state is optional so this is not a part of the reducer concept.

Thanks all for the fast responses and the 👍. It shows how important graphics are and how much easier they are to understand in comparison to text. Let me go through the comments one by one:

@timdorr Sorry, it's been made with a graphics program, therefore only png, no LaTeX, no SVG.

@sompylasar Thanks for all your suggestions:

  1. Root store={store} should be called Provider (if nothing changed since I learned react-redux)

    I agree the <Root /> is superfluous. It was there, since I wanted to conceptually dinstinguish between the root element and the <Provider />. But it did not add much and I have taken it out.

  2. "a new state triggers re-render" should point to the connected components, they subscribe() to the store object that is passed down from Provider via React context and provide new props to their wrapped components.

    The components subscribe to the store but they do so with the connect() function and not with the store.subscribe() method. The store.subscribe() method is for triggering listener functions, each time there is a new state. Whether of not a component is re-rendered is "decided" by the diff in React or by the specs in the component itself. This is why the arrow for a re-render points from the store to the App and not to the component.

  3. /* connect() copied to */ — connect() does not copy, it creates and returns a new component (which is SmartComponent01Connected) that uses (instantiates) the original SmartComponent01.

    Agree. "Copy" was the wrong word. Changed it into instantiate. I just wanted to express that a new component is returned by connect().

  4. The overall terminology has changed from "smart"/"dumb" to "connected"/"presentational" (please update me if I'm wrong here)

    I believe it is "container" and "presentational". I kept the names but added this into the comments.

  5. In DumbComponent01 it's better to name the "dispatch" prop "onClick" to align with the built-in components.

    I know that it is often done like you suggest, but I do not like it so much. I think, the dispatch() function is THE central function in Redux controlling the workflow, so I do not really like to bury it within other functions or map it to other names. Also, I find that dispatch is pretty expressive in terms of what happens in the code. On click, dispatch a command. So, I probably leave it as is. But I am happy to hear what others say.

  6. dispatch() calls rootReducer() with specified actionCreator — wrong, the actionCreator gets called before the dispatch in the user-land code (in the dispatchOnClick). It's the created action object that is returned from actionCreator and gets passed to dispatch. Not obvious because written in one line:

    I have re-worded this. The component calls dispatch() and dispatch() gets an actionCreator as an argument. Of course dispatch() executes the actionCreator function, but this is not what I meant. What I mean is that the dispatch functions calls the reducer within its function body and passes the action object resulting from the actionCreator call to the reducer as an argument.

  7. dispatch(actionC(...));
    // is the same as
    const action = actionC(...);
    dispatch(action);

    I wanted to put the least amount of code in the Cheat Sheet. But based on your suggestion, I have now added the payload. I assume that actionC was imported, so I wouldn't want a new const.

    • In legend: no usage of react-router and immutable, suggest removing to avoid confusion.
      slicing the state is optional so this is not a part of the reducer concept.

    This is a general point. The idea of the Cheatsheet is to show a "typical" workflow. I would consider it typical to have a <Router \> and immutable.js or the likes, not only because it is recommended but also because immutable really provides a nice and convenient handling and interface, it is fast and it prevents errors. So ideally, it should be used straight from the beginning as good practice. The same holds for slicing the store. I think the typical case is that the state of the store is sliced, that there exists a reducer per slice and that these reducers are combined with combineReducer(). Also, the naming convention to name slices and reducers alike is quite central. So, I actually believe, slicing is really core of Redux. A state in a store that is not sliced, is for me a specific case, i.e. a state with 1 slice only.

    • In legend: reducer() updates state and state slices in store — reducer itself does not (and must not) update anything, it has to be a pure function that returns a new state given the current state and an action;

    I agree. "Update" was not the best word, so I changed it to "new" state in order to not cause any confusion.

  8. it's redux store implementation that calls the reducer and updates the state during the dispatch;

    Exactly.

So, here comes the new Cheat Sheet. Further comments welcome!

react-redux-workflow-graphical-cheat-sheet-v101

"a new state triggers re-render" should point to the connected components, they subscribe() to the store object that is passed down from Provider via React context and provide new props to their wrapped components.

The components subscribe to the store but they do so with the connect() function and not with the store.subscribe() method. The store.subscribe() method is for triggering listener functions, each time there is a new state. Whether of not a component is re-rendered is "decided" by the diff in React or by the specs in the component itself. This is why the arrow for a re-render points from the store to the App and not to the component.

Right, but:

  1. the component created by connect() calls store.subscribe() internally;
  2. the re-render decided by React starts at the level of connect()-ed component, not at the App level; and yes, it turns out to be more complex when multiple connected components are nested within each other.

In DumbComponent01 it's better to name the "dispatch" prop "onClick" to align with the built-in components.

I know that it is often done like you suggest, but I do not like it so much. I think, the dispatch() function is THE central function in Redux controlling the workflow, so I do not really like to bury it within other functions or map it to other names. Also, I find that dispatch is pretty expressive in terms of what happens in the code. On click, dispatch a command. So, I probably leave it as is. But I am happy to hear what others say.

Maybe a misunderstanding here. Here's what I suggest, and that perfectly follows your logic: "On click, dispatch a command.":

<SmartComponent01 />
  dispatchOnClick() {
    const payload = { /* ... */ };
    this.props.dispatch(actionC(payload));
  }


      <DumbComponent01 onClick={this.dispatchOnClick} />
        render() {
          <input type="checkbox" onClick={this.props.onClick} />
        }

Imagine your DumbComponent01 has two buttons, each has to have its own handler:

<SmartComponent01 />
  dispatchFirst() {
    const payload = { /* ... */ };
    this.props.dispatch(actionFirst(payload));
  }
  dispatchSecond() {
    const payload = { /* ... */ };
    this.props.dispatch(actionSecond(payload));
  }


      <DumbComponent01 onFirstClick={this.dispatchFirst} onSecondClick={this.dispatchSecond} />
        render() {
          <button type="button" onClick={this.props.onFirstClick}>{'First'}</button>
          <button type="button" onClick={this.props.onSecondClick}>{'Second'}</button>
        }

This approach allows to test DumbComponent01 independently from redux by passing any stub as the event handler, and that's why the separation into "dumb"/"smart" was introduced in the first place.

function alertFirst() { alert('first'); }
function alertSecond() { alert('second'); }

<DumbComponent01 onFirstClick={alertFirst} onSecondClick={alertSecond} />

dispatch() calls rootReducer() with specified actionCreator — wrong, the actionCreator gets called before the dispatch in the user-land code (in the dispatchOnClick). It's the created action object that is returned from actionCreator and gets passed to dispatch. Not obvious because written in one line:

I have re-worded this. The component calls dispatch() and dispatch() gets an actionCreator as an argument. Of course dispatch() executes the actionCreator function, but this is not what I meant. What I mean is that the dispatch functions calls the reducer within its function body and passes the action object resulting from the actionCreator call to the reducer as an argument.

Unfortunately, "dispatch() gets an actionCreator as an argument." and "Of course dispatch() executes the actionCreator function" are wrong statements.
Note the difference:

dispatch(actionC(...));  // dispatch gets *the return value* of actionCreator as an argument
// versus
dispatch(actionC);  // dispatch gets actionCreator as an argument
dispatch(actionC(...));  // this line is executed as follows:
// 1: const theObjectReturnedFromActionC = actionC(...)  // `actionC` is called, returns an action object
// 2: dispatch(theObjectReturnedFromActionC)  // `dispatch` is called with what's been just returned from `actionC(...)`

So, dispatch does not execute the actionCreator function, it's the user-land code which executes it.

dispatch(actionC(...));
// is the same as
const action = actionC(...);
dispatch(action);

I wanted to put the least amount of code in the Cheat Sheet. But based on your suggestion, I have now added the payload. I assume that actionC was imported, so I wouldn't want a new const.

Note the difference between actionC (a reference to the actionCreator function) and actionC(...) (a call to the actionCreator function, evaluates to the return value of that call).

This comment was not about the payload, it was about differentiating between passing a reference to a function and passing a return value of a function call.

By the way, what's hidden behind the ... in the above snippet is not necessarily the action payload, this can be any data needed by the actionCreator to make the action object. The payload itself is generally created inside actionCreators. You could create a generic actionCreator that takes any payload and internally just wraps it into an action-like object but that is a different case, not for the guide you're drawing.

In legend: no usage of react-router and immutable, suggest removing to avoid confusion.
slicing the state is optional so this is not a part of the reducer concept.

This is a general point. The idea of the Cheatsheet is to show a "typical" workflow. I would consider it typical to have a <Router \> and immutable.js or the likes, not only because it is recommended but also because immutable really provides a nice and convenient handling and interface, it is fast and it prevents errors. So ideally, it should be used straight from the beginning as good practice. The same holds for slicing the store. I think the typical case is that the state of the store is sliced, that there exists a reducer per slice and that these reducers are combined with combineReducer(). Also, the naming convention to name slices and reducers alike is quite central. So, I actually believe, slicing is really core of Redux. A state in a store that is not sliced, is for me a specific case, i.e. a state with 1 slice only.

Sorry, I didn't notice the fromJS usage, so I take back immutable not being used in the diagram. I'm still not convinced that immutable is the recommended way though.

I agree with having slices as a typical setup and naming them alike subreducer names.

I don't believe react-router on its own relates to redux as close as the depicted flows; at least react-router-redux is missing from the picture then. And if you are about to expand on the router stuff, this will be a quite complex addition to the overall picture, so I'd suggest you don't.

Skimming through this, I would generally back up everything that @sompylasar has said so far. I definitely don't see any reason to bring routing into this, and it's probably better to leave Immutable.js out of it as well.

Also, I find that dispatch is pretty expressive in terms of what happens in the code. On click, dispatch a command. So, I probably leave it as is. But I am happy to hear what others say.

Of course you can use whatever conventions you want, but I believe naming the prop for your dumb components dispatch is going to lead to confusion, since it is using the same term for two very different things. In your example, your dispatch function is effectively a "bound action creator", which is completely different than store.dispatch.

Besides that, there are a couple of other reasons:

  1. It's not particularly common for a dumb component to only need to dispatch one type of action.
  2. Generally, dumb components shouldn't even know they're inside a redux app --- they just collect user interactions and pass them on to smart parents who handle them. When I write dumb components, I try to do so in an agnostic way, so that they can be consumed equally by redux-connected parents and non-redux-connected parents.

Well, thanks again, for the feedback and especially @sompylasar for the amount of QA and explanations you have provided.

@sompylasar I have no problem admitting, that I agree with all you comments. Also, I apologize I have not been exact when I was referring to the actionCreator in terms of return object vs call by reference. But I believe it was correctly expressed in the Cheatsheet.

So, I now assume that I have reflected all comments from @sompylasar and @naw in the new version 1.0.2. Please let me know otherwise.

@sompylasar, I have not understood, why you are suggesting something else then what follows, so this may be the only (?) point open:

dispatchOnClick() {
   /* ... */
   const payload = { /* ... */ }
   this.props.dispatch(actionC(payload))
}

I have been thinking of how to address your recommendations, to take 'immutable' and 'react-router' out. (I think 'react-router-redux' is not required when using 'react-router' in a Redux environment). As I mentioned in the beginning the Cheatsheet is also going to be part of an article. But in the end I have decided that I can always use an "extended" version for the article and leave a core version here.

So here is the next version:

react-redux-workflow-graphical-cheat-sheet-v102

So, I now assume that I have reflected all comments from @sompylasar and @naw in the new version 1.0.2. Please let me know otherwise.

Yes, looks like now it's clear. Just noticed one more typo: "makeSameCalculations" should probably say "sOme", not "sAme".

@sompylasar, I have not understood, why you are suggesting something else then what follows, so this may be the only (?) point open:

dispatchOnClick() {
   /* ... */
   const payload = { /* ... */ }
   this.props.dispatch(actionC(payload))
}

What I'm trying to explain here is that what you pass into actionC may be not exactly the payload you get in the "flux standard action" object shape: { type: 'ACTION_C', payload: /* here */ }. This means calling the argument a "payload" may be misleading.

I'd also rephrase "connect() instantiates new component" to say "rendered by component made by connect()" because in React world seems nobody says instantiate because "component" is assumed to be a class, but instance of a component is "element", not "component instance".

What I'm trying to explain here is that what you pass into actionC may be not exactly the payload you get in the "flux standard action" object shape: { type: 'ACTION_C', payload: /* here */ }. This means calling the argument a "payload" may be misleading.

Isn't this how actionCreators are defined in Redux? I have just replaced 'text' in the example with the more general term 'payload'? I am not saying that 'payload' is an action object.

What I'm trying to explain here is that what you pass into actionC may be not exactly the payload you get in the "flux standard action" object shape: { type: 'ACTION_C', payload: /* here */ }. This means calling the argument a "payload" may be misleading.

Isn't this how actionCreators are defined in Redux? I have just replaced 'text' in the example with the more general term 'payload'? I am not saying that 'payload' is an action object.

You're right. But we're trying to make things simpler for the readers. This implies not calling different things with the same word. The "payload" word has vague meaning. In the Flux world (Redux is part of it) the word "payload" has been reserved to designate the "additional" data that goes along the action type in the action object. But I am referring to this part:

Other than type, the structure of an action object is really up to you. If you're interested, check out Flux Standard Action for recommendations on how actions could be constructed.

There is a recommended shape of action objects called "flux standard action" which is:

{
  type: 'ACTION_SOMETHING_HAPPENED',
  payload: {
    // Some info about action.
    what: 'something',
    when: 14567893456,
  },
  meta: {
    // Some info about dispatch.
    dispatchStatus: 'OK',
  },
}

In that recommended shape, the "payload" is the exact object that goes into the created action.

But action creators do not necessarily take that object as an argument. They can take a subset of that payload, or something different that helps to make the actual payload

function messagePosted(userId, text) {
  return {
    type: 'ACTION_MESSAGE_POSTED',
    payload: {
      messageId: Math.random(),  // bad example of id creation
      messageText: text,
      userId: userId,
    },
  };
}

I'd like to add that in my redux apps I do not use actionCreators at all, I have a generic one that makes a flux standard action out of three arguments. But that's me, not the commonly recommended way. I use redux-saga so I don't need logic inside action creators, and generally the same action is dispatched from one or a few places so this does not make sense to abstract into a function.

import serializeError from 'serialize-error';
import assert from 'assert';


/**
 * Makes a "flux standard action" object.
 *
 * The `payload` and `meta` are made serializable.
 *
 * @param  {string}  type  Action type.
 * @param  {*}  [payload]  (optional) Action payload.
 * @param  {*}  [meta]  (optional) Action metadata.
 * @return {{type:string,payload:*,meta:*}}  A "flux standard action" object.
 */
export default function makeAction(type, payload, meta) {
  assert((type && typeof type === 'string'), 'makeAction: `type` is not a string.');

  // NOTE(@sompylasar): Cloning `payload` and `meta` to not be afraid to put `Error` objects in them.
  // NOTE(@sompylasar): Using `serializeError` to clone because it can kill circular references.
  return {
    type: type,
    payload: ( payload && typeof payload === 'object' ? serializeError(payload) : payload ),
    meta: ( meta && typeof meta === 'object' ? serializeError(meta) : meta ),
  };
}

Exactly. "payload" is the additional information that goes alongside the type as shown under Actions in the Cheatsheet.

And this is exactly what I have tried to express.

Your example reads:

function messagePosted(userId, text) {
  return {
    type: 'ACTION_MESSAGE_POSTED',
    payload: {
      messageId: Math.random(),  // bad example of id creation
      messageText: text,
      userId: userId,
    },
  };
}

I have tried to express this with a general interface to an actionCreator

const payload = 
  {
      messageId: Math.random(),  // bad example of id creation
      messageText: text,
      userId: userId,
  }

function messagePosted(payload) {
  return {
    type: 'ACTION_MESSAGE_POSTED',
    // payload: payload
    payload     // ES6
  };
}

@uanders What you've described is a very "special" case, not the general case at all.

It is definitely not generally true that action creators receive a single argument and return it verbatim as the payload key of the action object.

There are three simplifications you've made:

  1. That action creators always return a payload key (edit: ok, I see you're not really saying that)
  2. That action creators take a single argument
  3. That action creators insert that argument into their response verbatim.

In a typical redux app, none of those are true, so you don't want your chart to reinforce such ideas.

Of course, some simplifications are acceptable in a chart, but the particular simplifications you've chosen could give beginners the wrong impression.

@naw I think it is impossible to describe the general case in a Cheatsheet of one page, so there is always a compromise. I think the general case is well described in the doc. The main focus of the Cheatsheet is to go one full circle and show the mechanism. It does not claim at all that actionCreators are always constructed the way it is shown here. The ideas I am trying to get across are:

  1. Typically (not always) there are information that go alongside the type. They are often called payload.
  2. Somehow, you need to set this payload per action.

I don't feel, that the Cheatsheet is claiming at all that actionCreators only take a single argument. This is just the solution chosen here for brevity. However, to understand what you would suggest instead, would you prefer to have something like this?

//ActionCreators
actionA(id)
actionB(text)
actionC(payload)

//Actions
{type: ACTION_A,
 id}

{type: ACTION_B,
text}

{type: ACTION_C,
payload}

Ok, I see now that you're using payload as a stand-in for a specific thing like item, todo, etc. I've updated my comment above accordingly. Nevertheless, "payload" has a lot of connotations, so I'd still suggest not using it.

I would use foo, bar, baz, etc.

Perhaps something like this.

actionA(foo)
actionB(bar)
actionC(foo, bar)

{ type: ACTION_A, foo, ...otherStuff }
{ type: ACTION_B, foo: bar.foo }
{ type: ACTION_C, fooBars: [foo, bar] }

@naw, thanks for the suggestion:

actionA(foo)
actionB(bar)
actionC(foo, bar)

{ type: ACTION_A, foo, ...otherStuff }
{ type: ACTION_B, foo: bar.foo }
{ type: ACTION_C, fooBars: [foo, bar] }

I started to put it into the Cheatsheet, but then I had to change to much and suddenly I felt, that I was losing consistency and had to explain too much. So, I turned it back. Your suggestion is certainly more general than to just use a payload, but I am hesitant. Personally. --- and even though it is less general --- I find it much easier to understand and explain that everything necessary to compute the new state slice is sitting in a payload object. At least I find this a much clearer approach than to have a different interface for every actionCreator, where there is no space to explain this. But again, I am not suggesting, that this is generally a must, it is just one viable option.

Furthermore, I still do not see the principal difference between @sompylasar's makeAction:

function makeAction(type, payload, meta)

and "my" way of using an actionCreator with regard to the payload argument:

function createAction(payload)

Both functions have payload as a parameter, which has to be preloaded with everything that is necessary to compute the new state. I see a lot of advantages in always having the same payload argument per actionCreator and have some difficulties to see why you are objecting to this so much apart from being less general. The upside is that it is much more consistent on a one page Cheatsheet. For the time being, I leave you the latest version with the error corrections and brood a little more over your suggestions with respect to payload. Thanks a lot so far.

react-redux-workflow-graphical-cheat-sheet-v103

It's perfectly fine to have specialized conventions within a given application. If you want all of your action creators to have an argument literally named payload and to return an action object with a key literally named payload, that's ok. There are pros and cons to conventions, and each developer is going to land on different ones that work well for her or him. Over time, developers even change their own conventions.

One thing you'll want to ask yourself is whether you are building a cheat sheet for react-redux in general, or a cheat sheet for your own conventions.

If I were to build a cheat sheet, it would likely look a lot different than this, and that's OK because diversity in educational tools is good for the community. Please feel free to move forward with what seems best to you. I don't think there are any glaring errors.

With that said, one small thing I would change is the wording for "also providers connect() function to content of ". <Provider> doesn't provide connect. Instead, it provides pieces of the store
so that connect can get its hands on them. In particular, subscribe, dispatch, and getState

I'm good with the latest version (1.0.3) if the @naw 's note is fixed:

With that said, one small thing I would change is the wording for "also providers connect() function to content of ". <Provider> doesn't provide connect. Instead, it provides pieces of the store
so that connect can get its hands on them. In particular, subscribe, dispatch, and getState

But it actually provides the store object itself, not its pieces, via React context (in fact, it provides anything that you pass to its store prop). Use the Force, read the source please https://github.com/reactjs/react-redux/blob/ac01d706dd0b0542a0befd9cd5869c96cd2314dc/src/components/Provider.js#L29

I stand corrected.

I did look at the source, but I was mistakenly under the impression that childContextTypes and store shape limited what actually ended up in context. I guess I'm dumber than I look. 😄

RE: #2254 (comment)

Just for info, both propTypes, childContextTypes and contextTypes are meant to be development-only hints, and they should not change what actually gets passed down. I'm not sure though (looked up before but forgot) if the top-level keys of childContextTypes (store and subscription, without looking into its contents) are the only keys that get passed through.

Here is version 1.0.4, in which the Provider provides the store and not connect():

react-redux-workflow-graphical-cheat-sheet-v104

1.0.4 LGTM 👍

Sorry this is a little off topic now, but I would appreciate some clarification on context.

@sompylasar my impression is that you do have to define the top-level keys for context in childContextTypes (and contextTypes too, of course) even in production, due to the invariants. The shape/type doesn't have to match, but the keys at least have to be present.

Am I mistaken?

@naw Yes, my bad, haven't noticed that these aren't under __DEV__, you're right, sorry for the confusion.

Edited the original comment #2254 (comment)

@naw, @sompylasar, @markerikson: I have put the final cheatsheet into its own repo:

https://github.com/uanders/react-redux-cheatsheet

There you will also find the "extended" version of the cheat sheet with 'immutable" and 'react-router' added. Furthermore, there is the accompanying article which I may post on Medium if it has been quality assured after some time. Feel free to comment on the article as well if you like. I think your comments made this cheat sheet way better....