DEV Community

Zhenting
Zhenting

Posted on • Updated on

React Redux-Sagas Starter Guide

Super simple setup for redux-saga in React. It's very basic but works so let's get started.

~ Creating react app

npx create-react-app redux_project 

~ Installing Redux Saga packages

npm i axios react-redux redux redux-saga

We gonna delete everything except App.js and index.js. Clean up our App.js so it's just displaying simple text.

import React, { Component } from 'react';

class App extends Component {    

  render() {
    return(
      <div>
        Hello World
      </div>
    )
  }
}
export default App;

At this point we gonna need to do a little setup. We need to create 4 folders inside of our react-app. Under the src folder. Create the folder [actions] [api] [reducers] [sagas]. Each of these folders will contain code that will be connected to our react-app.

Let's create our actions in our actions folder called data.js

// actions types is object with the
// key: GET_DATA_REQUEST -> value: describes the action
export const Types = {
    GET_DATA_REQUEST: 'get_data_request',
}

// function that returns an object literal 
export const getDataRequest = () => ({
    type: Types.GET_DATA_REQUEST
})

We are going to create our reducers. Go into our [reducers] folder and create these two files. data.js and index.js. So what is a redux? A redux a store a place that contains the state of our application. It's like state in react but with a lot more complexity and functions. But before we can create our Redux we have to first create Types and actions.

inside the [reducers] data.js

import {Types} from '../actions/data';

// create initial state for reducers
const INIT_STATE = {
    test: "Hello world!"
}

// reducer function to transform state
export default function data(state = INIT_STATE, action) {
    return state
}

Next we have to fill out our index.js inside of our [reducers]

import {combineReducers} from 'redux';
import DataReducer from './data';

export default combineReducers({
    data: DataReducer
})

Okay even tho we see the code that says api call and payload we are not going to worry about that right now cause the Redux store doesn't have anything directly to do with api's it just stores data from api's. Let's connect our Redux to our App, at this point we are only worry about getting that INIT_STATE to render on our page. Go to our index.js page in [src] to connect our Redux.

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';

import reducers from './reducers';
import { Provider } from 'react-redux';
import { createStore } from 'redux';

const store = createStore(reducers);

// pass our store into our Provider
ReactDOM.render(
    // Provider pass data into our App
    <Provider store={store}>
        <App />
    </Provider>
, document.getElementById('root'));

We have wrapped our application with Redux by use and passing our Redux state as store. App lives under our Provider and is now ready to be connected to Redux. Open App.js.

import React, { Component } from 'react';
import { connect } from 'react-redux';

class App extends Component {    

  render() {
    return(
      <div>
        {this.props.data.test}
      </div>
    )
  }
}

// redux providing state takeover
const mapStateToProps = (state) => {
    console.log("App State ->", state);
    return {
      data: state.data
    }
}
export default connect(mapStateToProps, { })(App)

Redux Provider takes over our state and no longer allow us to change state, but now passes the data from Redux store into our application by using props and because we have access to props we can render that to our page. It now say "Hello Redux!".

Go inside the [actions] folder and create a file called data.js inside of the actions folder. Okay the pattern behind Redux is that try to think of the redux is a state with a giant (switch statement) filters out stuff and make sure that when it gets data in the form of an action "dispatch" it takes that specific "dispatch" and changes your application state is a specific way and gives your application a new state. So actions are special functions that creates an object to give to our Redux and by reading that object the Redux will know how to update our state.

// actions types is object with the
// key: GET_DATA_REQUEST -> value: describes the action
export const Types = {
    GET_DATA_REQUEST: 'get_data_request',
    GET_DATA_SUCCESS: 'get_data_success',
}

// function that returns an object literal 
export const getDataRequest = () => ({
    type: Types.GET_DATA_REQUEST
})
// key: payload will contain result of the api call
export const getDataSuccess = ({data}) => ({
    type: Types.GET_DATA_SUCCESS,
    payload: { data }
})

Okay now comes the tricky part...
THe way we are going to use Sagas with Redux is our application is with generators. So generators are functions that contains state and is very similar to our closure in JS. Where a function when run creates it's private variables so that it could be used later and can't be accessed by the scope outside it's own closure. Generator takes that idea one step further and is designed in a way that when you write the generator code you decide how it will return data by using the yield key word it returns some data but maintain it's state until a condition is met.

What we are going to do is create two generator functions the fetchData generator has the job of making the api calls in the future, which if successful we will then pass that data from the api response to our watchGetchData generator and if fetchData gives us the go ahead we will then pass that data to our Redux.

import { takeEvery, fork, put } from 'redux-saga/effects';
import * as actions from '../actions/data';

// create a generator function
function* fetchData() {
    try {
        yield put(actions.getDataSuccess( {data: "Saga Data!"} ))
    }catch(e) {
        console.log(e);
    }
}
function* watchFetchData() {
    // create watcher of fetchData function
    yield takeEvery(actions.Types.GET_DATA_REQUEST, fetchData);
}

const DataSagas = [
    fork(watchFetchData)
];

export default DataSagas;

make sure we fill in the code for index.js for [sagas] just like [reducers]

import { all } from 'redux-saga/effects';
import DataSagas from './data';

// combine all sagas
export default function* rootSaga() {
    yield all([
        ...DataSagas,
    ]);
}

change our [reducers] data.js code to return the new data being passed into redux from our sagas.

// reducer function to transform state
export default function data(state = INIT_STATE, action) {
    switch(action.type) {
        case Types.GET_DATA_SUCCESS: {
            console.log("redux -> ", action.payload.data)    
            return {
                test: action.payload.data
            }
        }
        default: return state;
    }
}

With our redux connected to our sagas we can now update our App.js code and see if our Sagas are working. We start the application by calling the getDataRequest action then our Sagas is called giving us the data back and passing that data back into our application.

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getDataRequest } from './actions/data'; 

class App extends Component {    
  componentDidMount() {
    this.props.getDataRequest();
  }

  render() {
    return(
      <div>
        <h1>
          {this.props.data}
        </h1>
      </div>
    )
  }
}

// redux providing state takeover
const mapStateToProps = (state) => {
    console.log("App State ->", state);
    return {
      data: state.data.test
    }
}
export default connect(mapStateToProps, { getDataRequest })(App)

The last step is to have our application make api calls from our saga generators.
Inside the [api] folder we are going to create data.js. All we are doing is making a get request to our test dummy db.

import axios from 'axios';

// data api calls
export const getData = () => {
    return axios.get('http://localhost:4000/comments')
}

Let's add in the api call into our saga

import { takeEvery, call, fork, put } from 'redux-saga/effects';
import * as actions from '../actions/data';
import * as api from '../api/data';

// create a generator function
function* fetchData() {
    // try to make the api call
    try {
        // yield the api responsse into data
        const data = yield call(api.getData);
        yield put(actions.getDataSuccess( {data: data.data} ))
    }catch(e) {
        console.log(e);
    }
}
function* watchFetchData() {
    // create watcher of fetchData function
    yield takeEvery(actions.Types.GET_DATA_REQUEST, fetchData);
}

const DataSagas = [
    fork(watchFetchData)
];

export default DataSagas;

And also update our redux to return the data from our api call

import {Types} from '../actions/data';

// create initial state for reducers
const INIT_STATE = {
    test: "Hello Redux!"
}

// reducer function to transform state
export default function data(state = INIT_STATE, action) {
    switch(action.type) {
        case Types.GET_DATA_SUCCESS: {
            console.log("redux -> ", action.payload.data.test)    
            return {
                test: action.payload.data.test
            }
        }
        default: return state;
    }
}

Okay we come full circle, our application call an action and it dispatched to our saga to make an ajax call and give that that back once the ajax call is completed and return that data to our redux store and it gets render to our page.
Last but not least let's post to our dummy db and get that data back once we have posted it.

First we create new post actions

// actions types is object with the
// key: GET_DATA_REQUEST -> value: describes the action
export const Types = {
    GET_DATA_REQUEST: 'get_data_request',
    GET_DATA_SUCCESS: 'get_data_success',
    GET_POSTS_REQUEST: 'get_posts_request',
    GET_POSTS_SUCCESS: 'get_posts_success',
    CREATE_POST_REQUEST: 'create_post_request',
}

// function that returns an object literal 
export const getDataRequest = () => ({
    type: Types.GET_DATA_REQUEST
})
// key: payload will contain result of the api call
export const getDataSuccess = ({data}) => ({
    type: Types.GET_DATA_SUCCESS,
    payload: { data }
})

// reading the posts
export const getPostsRequest = () => ({
    type: Types.GET_POSTS_REQUEST
})
export const getPostsSuccess = ({posts}) => ({
    type: Types.GET_POSTS_SUCCESS,
    payload: { posts }
})
export const createPostRequest = ({post}) => ({
    type: Types.CREATE_POST_REQUEST,
    payload: {post}
})

Update our api routes

import axios from 'axios';

// data api calls
export const getData = () => {
    return axios.get('http://localhost:4000/comments')
}
export const getPosts = () => {
    return axios.get('http://localhost:4000/posts')
}
export const createPost = ({post}) => {
    console.log("api call ->", post)
    let randomId = Math.random()*100;
    return axios.post('http://localhost:4000/posts',{"id":randomId, "msg": post + randomId})
}

Update our sagas

import { takeEvery, call, fork, put } from 'redux-saga/effects';
import * as actions from '../actions/data';
import * as api from '../api/data';

// create a generator function
function* fetchData() {
    // try to make the api call
    try {
        // yield the api responsse into data
        const data = yield call(api.getData);
        yield put(actions.getDataSuccess( {data: data.data} ))
    }catch(e) {
        console.log(e);
    }
}
function* watchFetchData() {
    // create watcher of fetchData function
    yield takeEvery(actions.Types.GET_DATA_REQUEST, fetchData);
}

function* fetchPosts() {
    try {
        const posts = yield call(api.getPosts);
        console.log(posts);
        yield put(actions.getPostsSuccess( {posts: posts.data} ))
    }catch(e) {
        console.log(e);
    }
}
function* watchFetchPosts() {
    yield takeEvery(actions.Types.GET_POSTS_REQUEST, fetchPosts);
}

function* createPost(action) {
    try {
        yield call(api.createPost, {post: action.payload.post});
        const posts = yield call(api.getPosts);
        yield put(actions.getPostsSuccess( {posts: posts.data} ))
    }catch(e) {
        console.log(e);
    }
}
function* watchCreatePost() {
    yield takeEvery(actions.Types.CREATE_POST_REQUEST, createPost);
}


const DataSagas = [
    fork(watchFetchData),
    fork(watchFetchPosts),
    fork(watchCreatePost)
];

export default DataSagas;

Last but not least update our App.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getDataRequest, getPostsRequest, createPostRequest } from './actions/data'; 

class App extends Component {    
  componentDidMount() {
    this.props.getDataRequest();
    this.props.getPostsRequest();
  }

  render() {
    return(
      <div>
        <h1>
          {this.props.data}
          {this.props.posts.map((e,i) => {
            return(
              <div key={i}>{e.msg}</div>
            )
          })}
        </h1>

        <button onClick={() => this.props.createPostRequest({"post": "Random Num Post - "})}>CLick to Add POst</button>
      </div>
    )
  }
}

// redux providing state takeover
const mapStateToProps = (state) => {
    console.log("App State ->", state);
    return {
      data: state.data.test,
      posts: state.data.posts
    }
}
export default connect(mapStateToProps, { getDataRequest, getPostsRequest, createPostRequest })(App)  

And Click that button we are good to go!

Top comments (2)

Collapse
 
sometimescasey profile image
Casey Juanxi Li • Edited

Hey, thanks so much for this tutorial. It's almost complete...

I was wondering why the console.log("redux -> ", action.payload.data.test) line never fired to change "Hello World" to "Saga Data". I think you're actually missing the step where you connect your saga as middleware when setting up the redux store.

redux-saga.js.org/

Ctrl-F "To run our Saga, we'll have to connect it to the Redux Store using the redux-saga middleware."

One more thing - for those of us who learned to use Redux by explicitly calling dispatch() on an action, you may want to explain that you are using the Object shorthand form of mapDispatchToProps to initially dispatch getDataRequest. I had some trouble figuring out why this action was actually dispatching in componentDidMount despite the fact that I never explicitly "dispatched" it in the code.

Otherwise, helpful tut! Thanks for taking the time to write this up!

Collapse
 
rotimibest profile image
Ibitoye Rotimi Best

Thanks for the article. I found it hard using your tutorial as a guide to setup my project because it wasn't clear what exact file you were showing and the format of your path is strange to me. You used [path][to][file.js], while in most tutorials (which is generally accepted) to use the unix file system format like: /path/to/file.js and that file path is placed before the code itself to make it clear what file was being changed.