Evheniy Bystrov

@evheniybystrov

React — redux for lazy developers

Each time working with redux in react app we spend a lot of time to make action types, action creators, reducers… Most actions in fact are CRUD for working with redux store.

Let’s imagine that someone makes it for us…

To make any react app we should create an empty project by using create-react-app or configure webpack with babel.

Usually I use webpack for own projects to make a custom configuration. But this time I’ll use create-react-app. If you don’t have it (I hope you installed node.js and npm on your PC) you can run next command:

npm i -g create-react-app

It will install the package as global so you can run it anywhere.

Let’s run create-react-app to create a new directory for our project and create react app:

create-react-app react-redux-example

Small notice…

For working with any node.js project you need to check package.json scripts section. For example, you can find start or test commands. And to run it you can make npm run start/ npm run test command or short way (for predefined commands) npm start / npm test.

Another way is using yarn. As it’s part of Facebook react ecosystem don’t forget to install it (npm i -g yarn). With yarn you can run the same commands: yarn start / yarn test.

And again, we are too lazy so we can just install another package — ntl:

npm i -g ntl

After that, you don’t need to research each package.json. Just run ntl command in a directory of node.js project and you can see all commands.

Comeback to our app…

We created a new react project. Let’s check it:

cd react-redux-example

To see all predefined commands run ntl:

ntl

We see start, build, test, eject commands:

  • start — starts our app,
  • build — builds static files (js, css…) to deploy it on server,
  • test — runs tests of our app,
  • eject — creates webpack config (after running eject command you’ll need to manage webpack by yourself).

So now we can run our app.

Run ntl and then select start command and press enter or make it manually by running npm start (yarn start) command (but it’s not our way).

As we can see it works on port: 3000. If we open it in a browser (http://localhost:3000/) we see our app:

As we can see from page info to update our app we should change src/App.js file.

Next, we should research our app structure and add redux.

To see the tree structure of our app run next command (without node_modules):

tree -I "node_modules"
.
├── README.md
├── package.json
├── public
│ ├── favicon.ico
│ ├── index.html
│ └── manifest.json
├── src
│ ├── App.css
│ ├── App.js
│ ├── App.test.js
│ ├── index.css
│ ├── index.js
│ ├── logo.svg
│ └── registerServiceWorker.js
└── yarn.lock

So we are ready to create a form example.

Our goals:

  • create stateful component by extending React.Component
  • see what is controlled components
  • see what is state and how to manage it
  • split our component: get stateless component and smart container
  • see what is HOC and check other tools for working with state (recompose)

So let’s create a new directory src/form and create index.js file with our form.

mkdir src/form
touch src/form/index.js

In real app, you will use eslint and the best config is airbnb eslint config. With all scope of rules, you should split code into js/jsx files. All JSX code you should keep in *.jsx files. After looking into project structure you will know what each file does.

But using create-react-app for our small lazy example we can make all code in *.js files.

Our simple form will be like this:

import React, { Component } from 'react';

class App extends Component {
render() {
return (
<form>
<h1>Our form example</h1>
<div>
<textarea />
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
);
}
}

export default App;

We need to update src/App.js for adding our form:

import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import Form from './form';

class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<Form />
</div>
);
}
}

export default App;

And run start command:

Here we just have a textarea input field and submit button.

We can enter any text and press submit.

But how can we get entered text? Now we have uncontrolled textarea component. To get entered text we can use react ref links but it’s not a react way. To work with react like a pro we need to use state.

Let’s update our component and make it smart:

import React, { Component } from 'react';

class App extends Component {
constructor(props) {
super(props);
this.state = {
text: '',
};
}

onTextChange = (event) => {
this.setState({ text: event.target.value });
};

render() {
return (
<form>
<h1>Our form example</h1>
<div>
<textarea onChange={this.onTextChange}>
{this.state.text}
</textarea>
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
);
}
}

export default App;

Now we can get entered value but what about submit button? We should process entered value:

import React, { Component } from 'react';

class App extends Component {
constructor(props) {
super(props);
this.state = {
text: '',
};
}

onTextChange = (event) => {
this.setState({ text: event.target.value });
};

onFormSubmit = (event) => {
event.preventDefault();
alert(this.state.text);
};

render() {
return (
<form onSubmit={this.onFormSubmit}>
<h1>Our form example</h1>
<div>
<textarea onChange={this.onTextChange}>
{this.state.text}
</textarea>
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
);
}
}

export default App;

I added form submit handler. If we enter some test text and press submit we see an alert with the entered text:

In real app, you can make ajax request instead of alert.

So we see what is controlled and uncontrolled components — in our case it’s textarea. And we see what is smart component or container.

In real production app if you want to be a pro you need to split logic into single responsibility components. In our case it’s splitting our big smart component into stateless component (or stupid component in some docs) and container.

We get at least two files each with own single responsibility: stateless component will be just a view layer and container will be responsible for the state.

Let’s create FormComponent, FormContainer and integrate it to our form:

touch src/form/formComponent
touch src/form/formContainer

And our code:

FormComponent:

import React, { Component } from 'react';

class FormComponent extends Component {
render() {
return (
<form onSubmit={this.props.onFormSubmit}>
<h1>Our form example</h1>
<div>
<textarea onChange={this.props.onTextChange}>
{this.props.text}
</textarea>
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
);
}
}

export default FormComponent;

FormContainer:

import React, { Component } from 'react';

const FormContainer = FormComponent => {
return class App extends Component {
constructor(props) {
super(props);
this.state = {
text: '',
};
}

onTextChange = (event) => {
this.setState({ text: event.target.value });
};

onFormSubmit = (event) => {
event.preventDefault();
alert(this.state.text);
};

render() {
return (
<FormComponent
text={this.state.text}
onTextChange={this.onTextChange}
onFormSubmit={this.onFormSubmit}
/>
);
}
}
};

export default FormContainer;

And our form:

import React, { Component } from 'react';
import FormContainer from './formContainer';
import FormComponent from './formComponent';

const Form = FormContainer(FormComponent);

class App extends Component {
render() {
return (
<Form />
);
}
}

export default App;

Here we see some interesting cases:

  • we just split code and for stateless FormComponent we put state and handlers as properties. FormContainer does not render jsx code. It just puts state and handlers to FormComponent.
  • FormContainer is not a react component. It’s high order component (HOC) — wrapper function. We put component as a parameter to function and it returns react component.

HOC is an interesting pattern for react development. You can write your app in totally function way.

Just imagine your react component can be just a JS function:

props => jsx

Don’t forget this arrow function.

Let’s update our form component and make it functional:

import React from 'react';

const FormComponent = props => (
<form onSubmit={props.onFormSubmit}>
<h1>Our form example</h1>
<div>
<textarea onChange={props.onTextChange}>
{props.text}
</textarea>
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
);

export default FormComponent;

Here we have a function. It gets props and returns JSX code. Less code, it’s more declarative.

But what about our container? Should it be still a class? Working with redux it can be totally functional. Redux is a storage for global state. In some cases you will work with local state and you will create a classes. But speaking about declarative code don’t forget that we are too lazy for writing all code by ourselves. Our container is a HOC and there are a lot of production ready HOCs.

The most interesting library for that is recompose. You can find a lot of examples if you open documentation.

Next, I want to show how to make our container more functional.

But first we should install it:

npm i -S recompose

or

yarn add recompose

After we can change our container. We will use withStateHandlers()

import { withStateHandlers } from 'recompose';

const FormContainer = withStateHandlers(
({ text = '' }) => ({ text }),
{
onTextChange: () => event => ({
text: event.target.value,
}),
onFormSubmit: ({ text }) => (event) => {
event.preventDefault();
alert(text);
},
},
);

export default FormContainer;

Again, less code, it’s more declarative. And in our case it’s functional. We even don’t need to import react because it’s just JS code.

If we run our app we see the same results:

So we just used ready recompose HOC for working with local state without making it manually.

We finished our goals for this part: we saw controlled and uncontrolled components, stateless(functional) and smart (stateful) components — containers. We created own HOC and found a lot of ready HOCs created by other developers to make our development more faster and make our lazy life more easier :)

Next step is adding redux…

To add redux in our app as any other dependency we should first install it. In our case we need redux and react-redux to use it with react.

yarn add redux react-redux

And one thing — redux-logger:

yarn add redux-logger

It’s useful for debugging.

Let’s create our store:

touch src/store.js

With next code:

import { createStore, applyMiddleware } from 'redux';
import logger from 'redux-logger';

import reducers from './reducers';

const store = createStore(
reducers,
applyMiddleware(logger)
);

export default store;

And reducers:

touch src/reducers.js

For now, it’s empty, but we will add reducer for our form component later:

import { combineReducers } from 'redux';

export default combineReducers({});

Next step is creating provider for app — src/index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import './index.css';
import App from './App';
import store from './store';
import registerServiceWorker from './registerServiceWorker';

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,

document.getElementById('root')
);
registerServiceWorker();

It makes the store available to all container components in the application without passing it explicitly. You only need to use it once when you render the root component.

And now we need to create action types, action creators, and reducer.

But before we need to think about our actions:

  • we need to change text
  • submit our form.

Action types:

touch src/form/types.js

With next code:

export const FORM_TEXT = '@@form/TEXT';
export const FORM_SUBMIT = '@@form/SUBMIT';

In real production app you can create a lot of types/actions and each time you will have a naming problem. So if you don’t want to have a collision problems you need to add suffix to your types. I use @@moduleName/TYPE notation. In our case for form module I use @@form/TYPE, for example: @@form/TEXT or @@form/SUBMIT.

Action creators:

touch src/form/actions.js

With code:

import { FORM_TEXT, FORM_SUBMIT } from './types';

export const textAction = text => ({
type: FORM_TEXT,
text,
});

export const submitAction = () => ({
type: FORM_SUBMIT,
});

And reducer:

touch src/form/reducer.js

With code:

import { FORM_TEXT } from './types';

const defaultState = {
text: '',
};

export default (state = defaultState, action) => {
switch (action.type) {
case FORM_TEXT:
return { ...state, ...action };
default:
return state;
}
};

And don’t forget to add this reducer to global reducers (in next articles I’ll show how to use code splitting and reducer injecting).

src/reducers.js:

import { combineReducers } from 'redux';

import form from './form/reducer';

export default combineReducers({ form });

Next step is changing form container:

import { connect } from 'react-redux';
import { textAction, submitAction } from './actions';

const mapStateToProps = state => state.form;
const mapDispatchToProps = { textAction, submitAction };

export default connect(mapStateToProps, mapDispatchToProps);

Connect is a HOC, created to connect store to components. It gets parameters (mapStateToProps and mapDispatchToProps) and returns HOC.

  • mapStateToProps is a selector, it filters data from the store.
  • mapDispatchToProps creates dispatchers for actions. Using dispatchers you can change state.

A small changes of form component:

import React from 'react';

const FormComponent = props => (
<form onSubmit={(event) => {
event.preventDefault();
props.submitAction();
alert(props.text);
}}>
<h1>Our form example</h1>
<div>
<textarea
onChange={event => props.textAction(event.target.value)}
value={props.text}
/>
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
);

export default FormComponent;

Not so nice code as the previous example but here we have a global state and it’s still functional.

So we are done. Let’s start app and see results:

And submit:

The same results.

As you can see we didn’t make reducer for form submit action. Because form submitting usually finishes by sending ajax request or making other side effects. And it’s an interesting point (redux thunk, redux-saga, redux-promise, redux-observable…) for another article.

All console text we see thanks to redux-logger.

Each time working with redux we need to make the same again and again: create action types, action creators, reducer, even container. Imagine if someone makes it for us.

Try my small project: redux-lazy.

Install it:

yarn add redux-lazy

Next create rl.js file:

touch src/form/rl.js

With next code:

import RL from 'redux-lazy';

const rl = new RL('form');
rl.addAction('text', { text: '' });
rl.addAction('submit', {});

const result = rl.flush();

export default result;

Now we can delete types.js, actions.js, reducer.js and form container. We have all this stuff from the box.

Update src/index.js

import React from 'react';

import FormComponent from './formComponent';
import rl from './rl';

const Form = rl.Container(FormComponent);

const App = () => <Form />;

export default App;

Here we make functional component and we get Container from redux-lazy.

src/reducers.js

import { combineReducers } from 'redux';

import rl from './form/rl';

export default combineReducers({ [rl.nameSpace]: rl.reducer });

Here we get namespace and reducer from redux-lazy.

And small updates in from component:

import React from 'react';

const FormComponent = props => (
<form onSubmit={(event) => {
event.preventDefault();
props.submitAction();
alert(props.text);
}}>
<h1>Our form example</h1>
<div>
<textarea
onChange={event => props.textAction({ text: event.target.value })}
value={props.text}
/>
</div>
<div>
<input type="submit" value="Submit" />
</div>
</form>
);

export default FormComponent;

And results:

You can get a lot of stuff from redux-lazy:

const {
nameSpace,
types,
actions,
defaultState,
reducer,
mapStateToProps,
mapDispatchToProps,
Container,
} = rl.flush();

So just to repeat:

  1. Create redux-lazy object: const rl = new RL(‘form’);
  2. Add actions: rl.addAction(‘text’, { text: ‘’ });
  3. Flush the code.

That’s all.

I think it’s useful for any (not only lazy) react/redux developer.

To make possibility run it on your PC I created a github repository.

To run it locally just make next steps:

git clone https://github.com/evheniy/react-redux-example.git
cd react-redux-example
yarn
ntl

More by Evheniy Bystrov

Topics of interest

More Related Stories