November 4, 2017 (7y ago)

Hướng dẫn xây dựng ứng dụng React Redux CRUD

Building a single-page CRUD app using React and Redux can be challenging because you’ll have to deal w/ new techniques and terms like “Reducers”, “Actions”, “Middlewares”, “Stores” and so on.

Perhaps the trickiest part is making async requests and handling responses. While there are many examples, there is no well established pattern for making async requests and handling responses in Redux apps(just yet).

In this blog I’ll provide a general approach on how to build a Blog app that has 3 pages and show navigate b/w them.

Further, I’ll also establish a pattern for making async requests and handling four async states: “loading”, “success”, “error” and “success-and-navigate”.

Source code: https://github.com/rajaraodv/react-redux-blog

Let’s get started.

STEP 1 — Write Detailed Mocks For Each Page And Phases.

In our app we have 3 pages: An Index page that shows a list of Posts, a Post details page and a New Post page.

Each page has “Success”, “Loading” and “Error” phases because they all make AJAX calls to load/delete posts, so we need to mock those things as well.

1.1 Success Phase — Detailed Mocks when things are working

Note: You can click on the pictures to Zoom and read

1.2 Loading Phase — Detailed Mocks when they are loading

1.3 Error Phase — Detailed Mocks when there is an Error

STEP 2 — Divide Each Page Into Components

Look at all the phases of each page and roughly divide each page into components based on “purpose” and based on physical location.

This helps you identify reusable components across different pages and also any additional ones in a specific phase. For example, you may need a “Spinner” or an “Error” component for different phases.

Note: this doesn’t need to be perfect. You can make changes later on.

2.1 — Success Phase: Divide Each Page Into Components

  1. Index page: 1. Shows list of posts, 2. Allows navigating to Post form page to create new post. So we end up w/ PostsList and Header components.
  2. Post details page: 1. Shows details of the post, 2. Allows navigating back to Index page 3. Index page. So we end up with PostDetails and Header components.

3. New Post page: 1. Allows creating posts and 2. allows navigating back to Index. Again we end up with PostForm and Header component.

Notice that we can reuse Header across all 3 pages because of the physical location. So we end up with four components (instead of 6): 1. PostsList, 2. PostDetails 3. PostForm and 4. Header components.

2.2 — Loading Phase: Divide Each Page Into Components

If you look at the mock, we are simply displaying “loading..” text in each page and not using some fancy spinner or “toast”. So we don’t have any more components.

2.3 — Error Phase: Divide Each Page Into Components

If you look at the mock, we are simply throwing an alert popup and not using any custom modals. So we don’t have any more components.

So net-net, we have 4 components (1. PostsList, 2. PostDetails 3. PostForm and 4. Header)

Redux Terms:

Redux Terms — “Actions” And “States”

Every component does two things: 1. Listen to the user and server events and send them to JS functions. In Redux, events are represented as a JSON object called “Actions”.

{"type": "FETCH_POST", "id": 1234} // <-- Action

2. Render DOM based on some data. This data is called a“state”, which is also a JSON object.

{"post": {"id": 1234, "title": "My Redux Post"}} // <-- state

Redux Terms — “Action Creators”

These are functions that listen to DOM or server events and return formal JSON “Action” object.

function fetchPost(id) {
  return {
    type: FETCH_POST,
    result: makeServerRequest('http://postsServer.com/api/id'),
  };
}

See Action Creators for our app.

Redux Terms — “Dispatching an Action”

Redux provides a function called “dispatch” which allows us to pass the “Action” JSON object to all other components. Dispatching an Action means simply calling the dispatch function w/ the action JSON object.

//Call the "Action Creator" w/ post's id and then use it's return //value (Action JSON object) to finally dispatch it to "reducers"dispatch(fetchPost(id)) or dispatch({type:"FETCH_POST", id:1234})

The components calls Action creators to receive Actions and then dispatches the actions. Redux then send the actions to “Reducers”

Redux Terms — “Reducers”

Reducers are functions that take an Action and the current state that was sent to them via “dispatch”, apply action to the current state and return a new state. And Redux re-renders all components whenever there is a new state.

//If the action is FETCH_POST_SUCCESS, return a new "activePost" //state w/ new post)
case FETCH_POST_SUCCESS:
 return {activePost: {post: action.payload.data, error:null, loading: false}};

See Reducers “reducer_posts.js” and main “index.js” (this combines multiple reducers into one)

OK, before we go ahead to step 3, let’s understand how to deal with Async Actions because every page makes AJAX calls.

PATTERN: Dealing With Async Actions

If component is loading an object(e.g. list of Posts) via AJAX call to the server, that object’s state should keep track of all the potential states. Initial state for such objects should look like: {**_objName: {obj:null, loading: false, error:null}}._**

Further, such components should dispatch up to 4 actions such as “FETCH_OBJ”(for loading), “FETCH_OBJ_SUCCESS”, “FETCH_OBJ_FAILURE” and “OBJ_RESET”(to cleanup dirty previous state).

For example, if we are loading list of posts..

Note: You can click on the pictures to zoom and read

Initial State: Initial state should look like,

{postsList:{posts:[], loading:false, error:null}}

Actions:

  1. FETCH_OBJ — Dispatch this to make the server request and also let other components know we are loading. This helps current/other components show “loading” or hide or do something.
dispatch({“type”: “FETCH_POSTS”, loading: true})

Once Redux gets this and passes it through reducers, the new state will look something like:

{postList: {posts:null, error: null, loading: true}}

2. FETCH_OBJ_SUCCESS: Dispatch this when you get successful response. This is to show the actual data and also to cancel “loading”

dispatch({"type": "FETCH_POSTS_SUCCESS", "posts":[post1, post2])

Once Redux gets this and passes it through reducers, the new state will look something like:

{postsList:{posts:[post1, post2], error:null, loading: false}}

3.FETCH_OBJ_FAILURE: Dispatch this when you get a failed response. This is to show some error message and also to cancel “loading”.

dispatch({ type: 'FETCH_POSTS_FAILURE', error: 'Error message' });

Once Redux gets this and passes it through reducers, the new state will look something like:

{postList:{posts:null, error:{msg: "Error msg"}, loading: false}}

4.RESET_OBJ: Dispatch this to reset the component’s state after success/failure. This is optional but can be useful when you want to reuse a “dirty” component from previous AJAX request.

dispatch({
  type: 'RESET_POST',
  loading: false,
  post: null,
  error: 'Error message',
});

Once Redux gets this and passes it through reducers, the new state will look something like:

{postList:{post:null, error:null, loading: false}}

STEP 3 — List State and Actions For Each Component (AND For Each Phase)

Take a look at each component one by one and each phase and list of state and actions.

We have 4 components: 1. PostsList, 2. PostDetails 3. PostForm and 4. Header components.

3.1 PostList Component — List State And Actions

States:

List out various data that may change the display of the component in all phases of the component.

  1. Shows list of Posts. Let’s call the state as “posts” (an array).
  2. Shows “Loading..”, if it’s in the processing fetching the posts. Let’s call this state “loading”(boolean)
  3. Shows “Error” if there is an error. Let’s call this state as “error”(null or error info).

Since all the above are related to PostList, let’s put them in a single state object called postList.

{ postsList: {posts: [], error:null, loading: false} //initial state

Actions:

This component makes a “AJAX” call to load posts, so we’ll use the above mentioned pattern and create 4 actions.

1.Asks server for list of posts. Let’s call this action as: “FETCH_POSTS”.

export function fetchPosts() {
  const request = axios.get(`${ROOT_URL}/posts`);
  return {
    type: FETCH_POSTS,
    payload: request,
  };
}

2.Tells every component that it received posts (success case). Let’s call this “FETCH_POSTS_SUCCESS”

export function fetchPostsSuccess(posts) {
  return {
    type: FETCH_POSTS_SUCCESS,
    payload: posts,
  };
}

3.Tells every component that there was an error(failure case). Let’s call this “FETCH_POSTS_FAILURE”

export function fetchPostsFailure(error) {
  return {
    type: FETCH_POSTS_FAILURE,
    payload: error,
  };
}

4. Resetting data is not required because this is the 1st page (you’ll see how this is useful in other 2 pages)

3.2 PostDetails Component — List State And Actions

Note: You can click on the pictures to zoom and read

3.3 PostForm Component — State And Actions

3.4 Header Component — List State And Actions

STEP 4 — Create Action Creators For Each Action

We have a total of 12 actions(4 actions x 3 pages), create action creators for each one. Please see the source code here.

//Example Action creators...
export function fetchPosts() {
 const request = axios.get(`${ROOT_URL}/posts`);

return {
 type: FETCH_POSTS,
 payload: request
 };
}
export function fetchPostsSuccess(posts) {
 return {
 type: FETCH_POSTS_SUCCESS,
 payload: posts
 };
}
...

Redux Term: “Reducers”

Reducers are functions that take “state” from Redux and “action” JSON object and returns a new “state” to be stored back in Redux.

1. Reducer functions are called by the “Container” containers when there is a user or server action. 2. If the reducer changes the state, Redux passes the new state to each component and React re-renders each component

The below function takes the current "postsList" inside "...state" and merges new "postList" and creates a new state(json), if the action is "FECTH_POSTS_SUCCESS"

case FETCH_POSTS_SUCCESS:
 return { …state, postsList: {posts: action.payload, error:null, loading: false} };

STEP 5 — Write Reducers For Each Action

We have 12 actions, we need to write reducers for each one of them.

Please look at the source code for details here.

Redux Term: “Presentational” and “Container” Components

Keeping React and Redux logic inside each component can make it messy, so Redux recommends creating a dummy presentation only component called “Presentational” component and a parent wrapper component called “Container” component that deals w/ Redux, dispatch “Actions” and more.

The parent Container then passes the data to the presentational component, handle events, deal with React on behalf of Presentational component.

Legend: Yellow dotted lines = “Presentational” components. Black dotted lines = “Container” components.

STEP 6 — Implement Every Presentational Component

We have 4 components: PostsList, PostDetails, PostForm and Header. Let’s create presentational components for each one.

6.1 Implement Presentational Component — PostsList

Note: You can click on the pictures to zoom and read

6.2 Implement Presentational Component — PostDetails

6.3 Implement Presentational Component — PostForm

Note: In the actual code, I am using the awesome redux-form library for form-validation. I’ll blog about it in a different post.

6.4 Implement Presentational Component — Header

Note: You can click on the pictures to zoom and read

STEP 7 — Create Container Component For Some/All Presentational Component

We have 4 components: PostList, PostDetails, PostForm and Header. Let’s create container components for each one.

7.1 Create Container Component — PostsListContainer

7.2 Create Container Component — PostDetailsContainer

7.3 Create Container Component — PostFormContainer

7.4 Create Container Component — HeaderContainer

STEP 8 — Finally Bring Them All Together

Below code is a simplified version of wiring everything together. Please see source code of the main index.js and reducers.js to get started.

import React from 'react'; <-- Main React lib
import ReactDOM from 'react-dom'; <-- Main React DOM lib
import { Provider } from 'react-redux';<-- Injects Redux to comps
import { createStore, applyMiddleware } from 'redux';<- Redux
import { Router, browserHistory } from 'react-router';<- Navigation
import reducers from './reducers'; <- Import reducers
import promise from 'redux-promise';
//Configure middleware w/ redux-promise for AJAX requests
const createStoreWithMiddleware = applyMiddleware(
  promise
)(createStore);

const store = createStoreWithMiddleware(reducers);
ReactDOM.render(
  <Provider store={store}> <- Inject global redux state to comps
    <Router history={browserHistory}>
       <Route path=/ component={App}> <- Wrapper for all pages
         <IndexRoute component={PostsIndex} /> <-wrapper Index page
         <Route path=”posts/new” component={PostsNew} /> <- New page
         <Route path=”posts/:id” component={PostsShow} /> <-Details
       </Route>
    </Router>
</Provider>
  , document.getElementById('body'));

That’s it for now!

Source: https://medium.com/@rajaraodv/a-guide-for-building-a-react-redux-crud-app-7fe0b8943d0f