02. Connecting Components with Redux
Okay, now I want to upgrade my todo apps. Let's say we want to create a profile page and display whatever name we insert into my todo pages. How to make our pages communicate each other? Redux is one solution!. Treats use Redux for its global state management. You may need to learn about Redux Concept, before you start this tutorial.
To use Redux in Treats projects, you don't need to add Provider and creating store for your app, because Treats will handle it. You just need to connect your component to Redux, define your reducer, and combine the reducers under _redux filesystem hooks. Redux is not mandatory in Treats. If you want to disable Redux, you can configure it in treats.config.js as instructed here.
Enough with the concept, let's put our hands to it!
Creating Redux Action, Types, and Initial State
- Create
profilefolder underpage. You may use Treats generator.
/* src/page/profile-page/profile.js */
import React, { Component } from "react";
import { injectIntl } from "@treats/intl";
import { connect } from "@treats/redux";
import style from "./profile.css";
/**
* Profile Page for Treats Tutorial
* @param props React props
* @param props.name
*
*/
class Profile extends Component {
constructor(props) {
super(props);
this.state = {
name: ""
}
}
handleFormSubmit = e => {
e.preventDefault();
const { name } = this.state;
//Save your component to Redux
}
handleInputChange = e => {
this.setState({
name: e.target.value
})
}
render() {
const { intl, name } = this.props,
{ name: temporaryName } = this.state;
return(
<div>
<h3>Hello, {name}</h3>
<form onSubmit={this.handleFormSubmit}>
<input value={temporaryName} onChange={this.handleInputChange} />
<button>{intl.formatMessage({id: "submit"})}</button>
</form>
</div>
)
}
}
const mapStateToProps = state => ({
/** YOUR MAP STATE TO PROPS FUNCTION */
});
const mapDispatchToProps = dispatch => ({
/** YOUR MAP DISPATCH TO PROPS FUNCTION */
});
export default connect(mapStateToProps,mapDispatchToProps)(injectIntl(Profile));
But where can I get the name props? Good question! We will get the name props from Redux state, and that's what mapStateToProps for. But we will implement it after creating our action and reducers.
- Create Redux boilerplate for profile. You can use Treats generator. In this tutorial, we will create it under
./src/redux.
src
|-- ...
|-- page
|-- redux
|-- profile-page
|-- action.js
|-- index.js
|-- initial-state.js
|-- reducer.js
|-- type.js
/* src/redux/profile-page/index.js */
import profileReducer from "./reducer";
export { default as profileActions } from "./action";
export { default as profileTypes } from "./type";
export { default as profileInitialState } from "./initial-state";
export default profileReducer;
- Add Redux's action type definition in
type.js.
/* src/redux/profile-page/type.js */
const types = {
/* Some other types */,
UPDATE_NAME: "UPDATE_NAME"
};
export default types;
- Implement our action in
action.js.
/* src/redux/profile-page/action.js */
import types from "./type";
/* Some other actions */
const updateName = name => ({
type: types[UPDATE_NAME],
name
})
export default {
/* Some other actions */,
updateName
};
- Add action handler in
reducer.js
/* src/redux/profile-page/reducer.js */
import initialState from "./initial-state";
import types from "./type";
const profileActionHandlers = {
/* Some other reducers */,
[types.UPDATE_NAME]: (state, action) => ({
...state,
name: action.name
})
};
const profileReducer = (state = initialState, action) =>
profileActionHandlers[action.type] ? profileActionHandlers[action.type](state, action) : state;
export default profileReducer;
- We need a proper initial state (not
undefined), so let's add some initial state ininitial-state.js.
/* src/redux/profile-page/initial-state.js */
const initialState = {
/* Some other states */,
name: "User"
};
export default initialState;
- Alright, all set in our Redux part. Let's continue into the next section.
Tweaking The Filesystem Hooks
Treats provide filesystem hooks for easy custom configuration and Redux is one of it. We already create our reducers for profile page, but the whole app only need a single reducer, since Treats only have a single store. Fortunately, there's some helpful method called combineReducers and Treats already wrap it in its dependencies.
- Create folder
_reduxundersrcand createindex.jsinside it.
src
|-- ...
|-- _redux
|-- reducer.js
- Write your
reducer.jsfile. In this step we will also add some alias to our apps intreats.config.jsso instead of writing../redux/profile-pagewe will write@redux/profile-page.
// treats.config.js
const path = require("path");
const config = {
....
alias: {
"@page": path.resolve(__dirname, "./src/page"),
"@redux": path.resolve(__dirname, "./src/redux")
}
};
module.exports = config;
/* src/_redux/reducer.js */
import { combineReducers } from "@treats/redux";
/* Other reducer */
import ProfileReducer from "@redux/profile-page";
export default combineReducers({
/* Other reducer */
profile: ProfileReducer
});
- All set for filesystem hooks, now let's get back to our unfinished components.
mapStateToProps and mapDispatchToProps
Okay in the first section, we promise to implement our mapStateToProps. Here we are:
- Implement our
mapStateToPropsinprofile.js
/* src/page/profile-page/profile.js */
...
const mapStateToProps = state => ({
name: state.profile.name
});
- Realize that our submit button doesn't do anything when clicked? Since we will push our new name to Redux, we are gonna dispatch
updateNamewe create earlier. Here, we will need to implementmapDispatchToProps.
/* src/page/profile-page/profile.js */
/* Other import */
import { profileActions } from "@redux/profile-page";
class Profile extends Component {
handleFormSubmit = e => {
e.preventDefault();
const { name } = this.state,
{ onFormSubmit } = this.props;
//Save your component to Redux
if (onFormSubmit) {
onFormSubmit(name);
}
}
/* Other class method */
}
const mapStateToProps = state => ({
name: state.profile.name
});
const mapDispatchToProps = dispatch => ({
onFormSubmit: name => {
dispatch(profileActions.updateName(name))
}
});
- All set to our components! Now check your redux page
Registering Page To Routes
Feels something missing in our route? You're sharp!. We need to register it to our route before testing it. Just like the previous tutorial, you need to add it in path.js, route.js, and module.js under _route.
- Add profile route name to
path.js. The route is up to you, but we will assign it with/profilefor this tutorial
/* src/_route/path.js */
export const TODO = "/";
export const PROFILE = "/profile";
- Add your route config in
route.js. Also deletedisabled: truein todo config, since it will trigger redirect. Redirect will cause Redux state back to initial state.
import { TODO, PROFILE } from "./path";
const route = [
{
name: "todo",
path: TODO,
exact: true
},
{
name: "profile",
path: PROFILE,
exact: true,
}
];
export default route;
- Connect your path and your page in
module.js
import Todo from "@page/todo";
import Profile from "@page/profile-page";
import { TODO, PROFILE } from "./path";
const module = {
[TODO]: Todo,
[PROFILE]: Profile
};
export default module;
- Next, add some navigation button that connect our pages. Note: please use
Linkinstead of<a>tag, because Redux state will be reset to initial state when we redirect using<a>tag.
/* src/page/todo/todo.js */
/* Other import */
import Link from "@treats/component/link";
class Todo extends Component {
/* Other method */
render() {
const { keyword, items } = this.state,
{ intl } = this.props;
return (
<div className={style.todo_form}>
<Link href="/profile" isPush>Change Name</Link>
<br />
<FormattedMessage
id="todo_title"
values={{
name: "User",
value2: 20
}}
/>
...
</div>
)
}
}
/* src/page/profile-page/profile.js */
/* Other import */
import Link from "@treats/component/link";
class Profile extends Component {
/* Other method */
render() {
const { name } = this.props,
{ name: temporaryName } = this.state;
return(
<div>
...
<Link href="/" isPush>Back to Todo</Link>
</div>
)
}
}
- To show the name in
todo.js, we need to connecttodo.jswith Redux. We only need to connecttodo.jswithmapStateToPropssince todo page will not be able to change the state. By connecting todo component, todo component will havenamein its props.
/* src/page/todo/todo.js */
class Todo extends Component {
...
render() {
const { keyword, items } = this.state,
{ intl, name } = this.props;
return (
<div className={style.todo_form}>
<Link href="/profile" isPush>Change Name</Link>
<br />
<FormattedMessage
id="todo_title"
values={{
name: name,
value2: 20
}}
/>
...
</div>
)
}
}
const mapStateToProps = state => ({
name: state.profile.name
})
export default connect(mapStateToProps)(injectIntl(Todo));
Now, the name on title will be changed after you change your name in profile page.
