Learn the Redux Architecture by Creating the Minimal TODO App on top of NEXT.js
Tags: react, redux, typescript, nextjs
Published: 2019-10-12 10:47:50

---
title: Learn the Redux Architecture by Creating the Minimal TODO App on top of NEXT.js
published: true
description: Learn the redux architecture by creating the minimal TODO app on top of NEXT.js. This is a minimal todo app with NEXT.js on the redux architecture which I explain step by step.
tags: react,redux,typescript,nextjs
cover_image: https://thepracticaldev.s3.amazonaws.com/i/l29ku2v4o8nw1bk70kgy.png
---

In this article, I'll explain the React Redux architecture by creating so simple TODO app which has just only two features (_ADD TODO_ and _DELETE TODO_).

This is a step by step guide of the example repo here:

{% github:https://github.com/saltyshiomix/nextjs-redux-todo-app %}

## The Point of View

- Minimal features
  - Add TODO
  - Delete TODO
- Only TypeScript
  - No database
  - No CSS
- We can learn these stacks:
  - [next](https://npm.im/next)
  - [react](https://npm.im/react)
  - [redux](https://npm.im/redux) and [react-redux](https://npm.im/react-redux)
  - [redux-thunk](https://npm.im/redux-thunk)
  - [reselect](https://npm.im/reselect)
  - [recompose](https://npm.im/recompose)

## Folder Structures

### NEXT.js Structures

```
.
├── components
│   ├── page.tsx
│   └── todo.tsx
├── next-env.d.ts
├── pages
│   ├── _app.tsx
│   └── index.tsx
└── tsconfig.json
```

### Redux Structures

```
.
├── actions
│   └── index.ts
├── components
│   ├── page.tsx
│   └── todo.tsx
├── constants
│   └── actionTypes.ts
├── containers
│   └── page.tsx
├── reducers
│   ├── index.ts
│   └── todo.ts
├── selectors
│   └── index.ts
└── store.ts
```

### Whole Structures

```
.
├── actions
│   └── index.ts
├── components
│   ├── page.tsx
│   └── todo.tsx
├── constants
│   └── actionTypes.ts
├── containers
│   └── page.tsx
├── next-env.d.ts
├── package.json
├── pages
│   ├── _app.tsx
│   └── index.tsx
├── reducers
│   ├── index.ts
│   └── todo.ts
├── selectors
│   └── index.ts
├── store.ts
└── tsconfig.json
```

## Step 1: Hello World

```bash
$ mkdir test-app
$ cd test-app
```

After that, populate `package.json` and `pages/index.tsx`:

**package.json**

```json
{
  "name": "test-app",
  "scripts": {
    "dev": "next"
  }
}
```

**pages/index.tsx**

```tsx
export default () => <p>Hello World</p>;
```

And then, run the commands below:

```bash
# install dependencies
$ npm install --save next react react-dom
$ npm install --save-dev typescript @types/node @types/react @types/react-dom

# run as development mode
$ npm run dev
```

That's it!

Go to `http://localhost:3000` and you'll see the `Hello World`!

## Step 2: Build Redux TODO App (suddenly, I see)

I don't explain Redux architecture! lol

Just feel it, separation of the _state_ and the view.

### Define Features (ActionTypes and Actions)

Define the id of the action type in the `constants/actionTypes.ts`:

```ts
export const TODO_ONCHANGE = 'TODO_ONCHANGE';
export const TODO_ADD = 'TODO_ADD';
export const TODO_DELETE = 'TODO_DELETE';
```

And in the `actions/index.ts`, we define the callbacks to the _reducers_:

(Just define arguments and return data. _Actions_ won't handle its _state_.)

```ts
import {
  TODO_ONCHANGE,
  TODO_ADD,
  TODO_DELETE,
} from '../constants/actionTypes';

export const onChangeTodo = (item) => ({ type: TODO_ONCHANGE, item });

export const addTodo = (item) => ({ type: TODO_ADD, item });

export const deleteTodo = (item) => ({ type: TODO_DELETE, item });
```

### State Management (Reducers)

In the `reducers/todo.ts`, we define the initial state and how to handle it:

```ts
import {
  TODO_ONCHANGE,
  TODO_ADD,
  TODO_DELETE,
} from '../constants/actionTypes';

export const initialState = {
  // this is a TODO item which has one "value" property
  item: {
    value: '',
  },
  // this is a list of the TODO items
  data: [],
};

export default (state = initialState, action) => {
  // receive the type and item, which is defined in the `actions/index.ts`
  const {
    type,
    item,
  } = action;

  switch (type) {
    case TODO_ONCHANGE: {
      // BE CAREFUL!!!
      // DON'T USE THE REFERENCE LIKE THIS:
      //
      //     state.item = item;
      //     return state; // this `state` is "previous" state!
      //
      // Please create a new instance because that is a "next" state
      //
      return Object.assign({}, state, {
        item,
      });
    }

    case TODO_ADD: {
      // if the `item.value` is empty, return the "previous" state (skip)
      if (item.value === '') {
        return state;
      }

      return Object.assign({}, state, {
        // clear the `item.value`
        item: {
          value: '',
        },
        // create a new array instance and push the item
        data: [
          ...(state.data),
          item,
        ],
      });
    }

    case TODO_DELETE: {
      // don't use `state.data` directly
      const { data, ...restState } = state;

      // `[...data]` means a new instance of the `data` array
      // and filter them and remove the target TODO item
      const updated = [...data].filter(_item => _item.value !== item.value);

      return Object.assign({}, restState, {
        data: updated,
      });
    }

    // do nothing
    default: {
      return state;
    }
  }
};
```

And next, define `reducers/index.ts` which combines all _reducers_:

(currently only one reducer, yet)

```ts
import { combineReducers } from 'redux';
import todo, { initialState as todoState } from './todo';

export const initialState = {
  todo: todoState,
};

export default combineReducers({
  todo,
});
```

### Create the Store

We define the _one_ store so that we can access any states from the store.

And pass the store to the page: with the NEXT.js, `pages/_app.tsx` is one of the best choices.

**store.ts**

```ts
import thunkMiddleware from 'redux-thunk';
import {
  createStore,
  applyMiddleware,
  compose,
  Store as ReduxStore,
} from 'redux';
import { createLogger } from 'redux-logger';
import reducers, { initialState } from './reducers';

const dev: boolean = process.env.NODE_ENV !== 'production';

export type Store = ReduxStore<typeof initialState>;

export default (state = initialState): Store => {
  const middlewares = dev ? [thunkMiddleware, createLogger()] : [];
  return createStore(reducers, state, compose(applyMiddleware(...middlewares)));
};
```

**pages/_app.tsx**

```tsx
import { NextPageContext } from 'next';
import App from 'next/app';
import withRedux from 'next-redux-wrapper';
import { Provider } from 'react-redux';
import store, { Store } from '../store';

interface AppContext extends NextPageContext {
  store: Store;
}

class MyApp extends App<AppContext> {
  render() {
    const { store, Component, ...props } = this.props;
    return (
      <Provider store={store}>
        <Component {...props} />
      </Provider>
    );
  }
}

export default withRedux(store)(MyApp);
```

### Compose the Pages

First, define selectors to avoid deep nested state:

```ts
import { createSelector } from 'reselect';

export const selectState = () => state => state.todo;

export const selectTodoItem = () =>
  createSelector(
    selectState(),
    todo => todo.item,
  );

export const selectTodoData = () =>
  createSelector(
    selectState(),
    todo => todo.data,
  );
```

Second, use that selectors and pass them to the container with the actions:

**containers/page.ts**

```ts
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import {
  compose,
  pure,
} from 'recompose';
import {
  onChangeTodo,
  addTodo,
  deleteTodo,
} from '../actions';
import {
  selectTodoItem,
  selectTodoData,
} from '../selectors';
import Page from '../components/page';

export default compose(
  connect(
    createSelector(
      selectTodoItem(),
      selectTodoData(),
      (item, data) => ({ item, data }),
    ),
    {
      onChangeTodo,
      addTodo,
      deleteTodo,
    },
  ),
  pure,
)(Page);
```

Third, implement the page component:

**components/page.tsx**

```tsx
import React from 'react';
import { compose } from 'recompose';
import Todo from './todo';

const Page = (props) => {
  // defined in the `containers/page.ts`, so the `props` is like this:
  //
  // const {
  //   item,
  //   data,
  //   onChangeTodo,
  //   addTodo,
  //   deleteTodo,
  // } = props;
  //
  return <Todo {...props} />;
};

export default compose()(Page);
```

Implement **components/todo.tsx**:

```tsx
import React from 'react';
import { compose } from 'recompose';

const Todo= (props) => {
  const {
    item,
    data,
    onChangeTodo,
    addTodo,
    deleteTodo,
  } = props;

  return (
    <React.Fragment>
      <h1>TODO</h1>
      <form onSubmit={(e) => {
        e.preventDefault();
        addTodo({
          value: item.value,
        });
      }}>
        <input
          type="text"
          value={item.value}
          onChange={e => onChangeTodo({
            value: e.target.value,
          })}
        />
        <br />
        <input
          type="submit"
          value="SUBMIT"
          style={{
            display: 'none',
          }}
        />
      </form>
      <hr />
      {data.map((item, index) => (
        <p key={index}>
          {item.value}
          {' '}
          <button onClick={() => deleteTodo(item)}>
            DELETE
          </button>
        </p>
      ))}
    </React.Fragment>
  );
};

export default compose()(Todo);
```

### Rewrite `pages/index.tsx`

Finally, update `pages/index.tsx` like this:

```tsx
import {
  NextPageContext,
  NextComponentType,
} from 'next';
import { compose } from 'recompose';
import { connect } from 'react-redux';
import Page from '../containers/page';
import { addTodo } from '../actions';
import { Store } from '../store';

interface IndexPageContext extends NextPageContext {
  store: Store;
}

const IndexPage: NextComponentType<IndexPageContext> = compose()(Page);

IndexPage.getInitialProps = ({ store, req }) => {
  const isServer: boolean = !!req;

  // we can add any custom data here
  const { todo } = store.getState();
  store.dispatch(addTodo(Object.assign(todo.item, {
    value: 'Hello World!',
  })));

  return {
    isServer,
  };
}

export default connect()(IndexPage);
```

![An image of the TODO app](https://thepracticaldev.s3.amazonaws.com/i/60xfbsijyphzjos2efb2.png)

#### `TODO_ONCHANGE`:

![An image of TODO_ONCHANGE](https://thepracticaldev.s3.amazonaws.com/i/4buj0tttjgupnvyfd9hf.png)

#### `TODO_ADD`:

![An image of TODO_ADD](https://thepracticaldev.s3.amazonaws.com/i/hax37ihqqrzwmx9cu27t.png)

#### `TODO_DELETE`:

![An image of TODO_DELETE](https://thepracticaldev.s3.amazonaws.com/i/92x14ulhz6s2v0mu7i2n.png)

## Conclusion

> Practice makes perfect.

Thank you for your reading!