We are now at a stage where syntax is no longer an issue. Even if they are, it would take us less than 1 minute to search for the correct syntax and example, another 5 to look for special use case on stackoverflow and maybe 1 hour or more to debug and actually able to apply the solution into our app. Then clear our memory in 2 seconds after. The point is, solving seperated, detailed and specific programming problem is no longer "impossible". We could mix and match, trial and error or coppy and paste our way to make rooks, bricks and steel. But building a whole application, a building, or even a skyscrapper seem to be an overwhelming start. The first question - yes after several weeks of learning to code we have built the habbit to ask, curios and doubting about everything even our life choices - is :

- Where to start ?

Redux is a tool for managing application state. It's a very common tool used in React applications.

Why Redux?

As your project grows with more and more features, it becomes really difficult to trace state changes through our app. The functions that changed our application state were scattered through several React components.

React has ‘unidirectional data flow.' This means components send state downwards as props.

When our components need to share state, but they don't share a parent-child relationship. We solve this issue by ‘Lifting state'. This means that we lift the state (and functions that change this state) to the closest ancestor(Container Component). We bind the functions to the Container Component and pass them downwards as props. This means that child components can trigger state changes in their parent components, which will update all other components in the tree.

But there are pain points we will start to feel as we scale our application up:

If you are starting to run into some of the above issues, it may mean you are ready for Redux.

Redux helps we deal with shared state management, but like any tool, it has tradeoffs. There are more concepts to learn, and more code to write. It also adds some indirection to our code, and asks we to follow certain restrictions. It's a trade-off between short term and long term productivity.

React state management data flow

React

Not all apps need Redux. Take some time to think about the kind of app we're building, and decide what tools would be best to help solve the problems we're working on.

Redux

The most common complaint about Redux is that it's quite verbose and can take some time to get your head around. It requires developers to explicitly describe how application state is updated through actions and reducers. It allows you to maintain the application state as a single global store, rather than in local component state. Here is a quick overview of how Redux is structured.

redux-diagram

Actions

// Action must have type
{
  type: "USER_LOGGED_IN",
  username: "dave"
}

Reducer

// Pure function
function square(x) {
  return x * x;
}
// Pure functions do not modify the values passed to them.
function squareAll(items) {
  return items.map(square);
}

// Impure functions
// On the opposite, impure functions may call the database or the network,
// they may have side effects, they may operate on the DOM,
// and they may override the values that you pass to them.
function square(x) {
  updateXInDatabase(x);
  return x * x;
}
function squareAll(items) {
  for (let i = 0; i < items.length; i++) {
    items[i] = square(items[i]);
  }
}

Store

The whole global state of your app is stored in an object tree inside a single store. The only way to change the state tree is to create an action, an object describing what happened, and dispatch it to the store. To specify how state gets updated in response to an action, you write pure reducer functions that calculate a new state based on the old state and the action.

import { createStore } from "redux";

/**
 * This is a reducer - a function that takes a current state value and an
 * action object describing "what happened", and returns a new state value.
 * A reducer's function signature is: (state, action) => newState
 *
 * The Redux state should contain only plain JS objects, arrays, and primitives.
 * The root state value is usually an object.  It's important that you should
 * not mutate the state object, but return a new object if the state changes.
 *
 * You can use any conditional logic you want in a reducer. In this example,
 * we use a switch statement, but it's not required.
 */
function counterReducer(state = { value: 0 }, action) {
  switch (action.type) {
    case "incremented":
      return { value: state.value + 1 };
    case "decremented":
      return { value: state.value - 1 };
    default:
      return state;
  }
}

// Create a Redux store holding the state of your app.
// Its API is { subscribe, dispatch, getState }.
let store = createStore(counterReducer);

// You can use subscribe() to update the UI in response to state changes.
// Normally you'd use a view binding library (e.g. React Redux) rather than subscribe() directly.
// There may be additional use cases where it's helpful to subscribe as well.

store.subscribe(() => console.log(store.getState()));

// The only way to mutate the internal state is to dispatch an action.
// The actions can be serialized, logged or stored and later replayed.
store.dispatch({ type: "incremented" });
// {value: 1}
store.dispatch({ type: "incremented" });
// {value: 2}
store.dispatch({ type: "decremented" });
// {value: 1}

Instead of mutating the state directly, you specify the mutations you want to happen with plain objects called actions. Then you write a special function called a reducer to decide how every action transforms the entire application's state.

In a typical Redux app, there is just a single store with a single root reducing function. As your app grows, you split the root reducer into smaller reducers independently operating on the different parts of the state tree. This is exactly like how there is just one root component in a React app, but it is composed out of many small components.

This architecture might seem like a lot for a counter app, but the beauty of this pattern is how well it scales to large and complex apps. It also enables very powerful developer tools, because it is possible to trace every mutation to the action that caused it. You can record user sessions and reproduce them just by replaying every action.

That was theory, the following exercise will ultilize these 2 hooks from react-redux

Requirements

Clone the following repository - it's like the create-react-app starter kit, with small additions.

git clone https://github.com/coderschool/redux-fundamentals-example-app

Then run yarn, to install dependencies, or npm start should show you the following:

preview

We'll need one extra library:

yarn add react-redux

Install that library, and now open your index.js. Add the following import at the top:

import { Provider } from "react-redux";

You need to wrap your entire project in a Provider similarly to how with React Router, we wrapped our App in a Router.

Change the code at the bottom of index.js to do so:

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

Note the store={store}. This is what makes the store available to all components inside of our App, but we need to first define store. Let's do that in a separate file.

Above that block, add:

import store from './store'

And create a new file called store.js in the same directory as index.js.

Inside our store, we'll try to keep it as simple as possible (this is where we start to diverge heavily from the official tutorial, by the way):

import { createStore } from "redux";

const initialState = {
  todos: [
    { id: 1, text: "Learn Redux" },
    { id: 2, text: "Make my teacher proud" },
  ],
};

const reducer = (state = initialState, action) => {
  return state;
};

const store = createStore(reducer);

export default store;

Negative : Normally we'd keep our reducer in a separate file, as the logic in the reducer is generally the most complicated. We'll do that in future weeks, but for now, we'll just keep it all in store.js.

We'll want a list of items, that displays each todo items. We'll create two components: TodoList and TodoListItem.

Create a components directory in your src/ folder, and create the two components.

src/components/TodoList.js:

import React from "react";

const TodoList = () => {
  return <div></div>;
};

export default TodoList;

src/components/TodoListItem.js just takes one prop for now, the todo, and displays it:

import React from "react";

const TodoListItem = ({ id, text }) => {
  return <div>{text}</div>;
};

export default TodoListItem;

Our TodoList component won't need props passed to it from Redux, and in fact, can just access our "store" directly.

TodoList.js:

import React from "react";
import { useSelector } from "react-redux";
import TodoListItem from "./TodoListItem";

const selectTodos = (state) => state.todos;

const TodoList = () => {
  const todos = useSelector(selectTodos);

  return (
    <ul className="todo-list">
      {todos.map((todo) => (
        <TodoListItem key={todo.id} {...todo} />
      ))}
    </ul>
  );
};

export default TodoList;

Bravo. Note how the data is now coming magically from the initialState that we defined in store.js! We never had to pass any sort of prop to TodoList from App.js.

Now we'll need a simple way to add todos. Let's create a new (ugly) component in src/components/Header.js:

import React, { useState } from "react";
import { useDispatch } from "react-redux";

const Header = () => {
  const [text, setText] = useState("");

  const handleChange = (e) => setText(e.target.value);

  const handleKeyDown = (e) => {
    // If the user pressed the Enter key:
    const trimmedText = text.trim();
    if (e.which === 13 && trimmedText) {
      alert("adding todo: " + trimmedText);
    }
  };

  return (
    <header className="header">
      <input
        className="new-todo"
        placeholder="What needs to be done?"
        value={text}
        onChange={handleChange}
        onKeyDown={handleKeyDown}
      />
    </header>
  );
};

export default Header;

Nothing is new here, except the fact that we listen to the "enter" key to submit a todo (we could also listen to a form submit, if we wrapped everything in a form). For now though, we just alert the user, instead of sending the actual data through.

Add this Header component above TodoList in App.js.

Pressing enter in the field should pop up a simple alert:

Now we just need to do one final step: connect our app to the store to add todos. Go back to Header.js, and add (the first line should be there, with text, setText but the useDispatch is new):

const [text, setText] = useState("");
const dispatch = useDispatch();

You'll need to import:

import { useDispatch } from "react-redux";

Now replace the alert with the following line:

dispatch({ type: "addTodo", payload: trimmedText });
setText("");

That second line is just to clear the box when we're done (remember this should go inside the if statement checking to see if the enter was pressed).

And finally - this is the hard part. Go back to store.js, and add in this logic:

const reducer = (state = initialState, action) => {
  if (action.type === "addTodo") {
    return {
      todos: [...state.todos, { id: 3, text: action.payload }],
    };
  }
  return state;
};

Negative : We definitely should not hard-code the value 3. You should find the max id, and add one to it. That brainteaser is left to you!

Now, your app should be complete.

Go consult the official tutorial and add in additional features. Demo here and instructions here.