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