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
profile
folder 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
_redux
undersrc
and createindex.js
inside it.
src
|-- ...
|-- _redux
|-- reducer.js
- Write your
reducer.js
file. In this step we will also add some alias to our apps intreats.config.js
so instead of writing../redux/profile-page
we 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
mapStateToProps
inprofile.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
updateName
we 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/profile
for this tutorial
/* src/_route/path.js */
export const TODO = "/";
export const PROFILE = "/profile";
- Add your route config in
route.js
. Also deletedisabled: true
in 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
Link
instead 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.js
with Redux. We only need to connecttodo.js
withmapStateToProps
since todo page will not be able to change the state. By connecting todo component, todo component will havename
in 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.