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:
saltyshiomix / nextjs-redux-todo-app
A minimal todo app with NEXT.js on the redux architecture
Features
- Minimal but well structured
- No CSS, only TypeScript
- We can learn these stacks:
Usage
# installation
$ git clone https://github.com/saltyshiomix/nextjs-todo-app.git
$ cd nextjs-todo-app
$ yarn (or `npm install`)
# development mode
$ yarn dev (or `npm run dev`)
# production mode
$ yarn build (or `npm run build`)
$ yarn start (or `npm start`)
The Point of View
- Minimal features
- Add TODO
- Delete TODO
- Only TypeScript
- No database
- No CSS
- We can learn these stacks:
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
$ mkdir test-app
$ cd test-app
After that, populate package.json
and pages/index.tsx
:
package.json
{
"name": "test-app",
"scripts": {
"dev": "next"
}
}
pages/index.tsx
export default () => <p>Hello World</p>;
And then, run the commands below:
# 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
:
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.)
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:
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)
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
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
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:
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
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
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:
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:
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);
TODO_ONCHANGE
:
TODO_ADD
:
TODO_DELETE
:
Conclusion
Practice makes perfect.
Thank you for your reading!
Top comments (8)
Hi, Nice article. Thank you.
I would like to use redux dev tools with this in development, but I am finding it difficult to setup.
Here is the link to the tool documentation. github.com/zalmoxisus/redux-devtoo...
Can you help take a look?
Thanks, I have figured this out.
What I did!
yarn add redux-devtools-extension
)import { composeWithDevTools } from 'redux-devtools-extension';
)const composeEnhancers = composeWithDevTools({});
)composeEnhancers(applyMiddleware(...middlewares))
)Update Store.ts file
Yes, you have done right!
Or just replace
compose
withcomposeWithDevtools
like this commit:Thanks, Do you have an idea of how to include SCSS to this setup?
This is a NEXT.js side.
Please see github.com/zeit/next.js/tree/canar...
Heard nextjs and redux: will try later tonight. Sounds like a lot of fun! Thanks for posting this!!
^^
Thank you for your comment!
If you have any advices, feel free to contact me :)
thanks this was so helpfull