DEV Community

Tulsi Prasad
Tulsi Prasad

Posted on

Making a project using React and Redux to build our grasp

Hey everyone! It's that time of the week when we put together all our previously gained knowledge of how Redux works effectively and how to update the state using Redux immutably. This has been a game changer in making real world applications and I'm gonna help you to get started with implementing Redux's library with your React application.

What are we going to make?

This is gonna be pretty simple but loaded with many actions and reducer just to make your concepts clear. It's a Countdown timer app. Instead of saying you a ton, let me show you the exact thing we're gonna build.

https://i.imgur.com/vhBFzGY.png

Amazed? 😜 This will need us to create various actions to make it run. As you can see, we have all these laps that's gonna come when we click on Lap and it also deletes them on click and resets the timer to zero when hit Reset. And also you can use the + and - keys to set the time for the timer. You can basically, try out the whole app now (to make things clear), as it's already deployed, at here.

Getting Started

Firstly, we need to build the functioning of the app and write the logic. And in my case, I did the entire app using primitive states first (not using Redux), just to understand its working better. And then I converted my state management to using Redux.

So for you to start, I have uploaded the primitive state code in the master branch and the app using Redux is in a separate branch named, state/redux. To get started, you can clone the master branch and follow along to add Redux state mangament to our app.

Link to GitHub Repo:

GitHub logo heytulsiprasad / redux-timer

A plain little countdown timer, made for my series of Redux posts on Dev.to.

Getting Started

Front Page of App

The app in the master branch, doesn't use Redux already. I made this project in a primitive way first, to have a clear understanding and once it was done, I moved to the state/redux branch to implement Redux.

A plain little countdown timer, made for my series of Redux posts on Dev.to

We Learn Redux

master branch

  • Clone: git clone https://github.com/heytulsiprasad/redux-timer.git
  • Setup environment: yarn or npm install (depending upon your fav package manager)
  • Running the app: yarn start or npm start

state/redux branch

  • Clone: git clone --single-branch state/redux https://github.com/heytulsiprasad/redux-timer.git
  • Setup environment: yarn or npm install (depending upon your fav package manager)
  • Running the app: yarn start or npm install

This project was bootstrapped with the CRA Template.

Reach out to Me 🙋‍♂️

Twitter Profile



Note: We're only going to focus on implementing Redux to this and not building the whole application from scratch. So, I do recommed once going through the main components to at least know which function does what, so it'd be easier to follow along.

Basic Working

All the functionality we need happens with the click of a button so, we need to pass an on click handler function to each of our custom Button component. with the clicked prop.

<Button clicked={this.incTimer}>+</Button>
<Button clicked={this.startTimer}>Start</Button>
<Button clicked={this.stopTimer}>Stop</Button>
<Button clicked={this.lapTimer}>Lap</Button>
<Button clicked={this.resetTimer}>Reset</Button>
<Button clicked={this.decTimer}>-</Button>
Enter fullscreen mode Exit fullscreen mode

If you're wondering what is the Button component, this is a look into that:

function Button(props) {
    return <button onClick={props.clicked}>{props.children}</button>;
}
Enter fullscreen mode Exit fullscreen mode

Designing the Store

From our previous Redux knowledge, we know our entire app needs to have a global store which will store all of the state data. So, lets decide how the structure of our store would be.

Firstly, create store folder at the root directory, to hold the necessary actions and reducers.

Now, if you looked into the state of our readymade app in the master branch, which is:

this.state = { time: { h: 0, m: 0, s: 0 }, seconds: 0, laps: [] };
Enter fullscreen mode Exit fullscreen mode

We have all the laps stored in the laps array and everything related to time is stored both in seconds and time values. So, to make things clear we can here make two different reducers inside our store folder, viz. laps.js and timer.js Also, we shall keep them inside a folder named reducers inside our store folder.

If you're curious, here's a snap of the file structure, from the final project.

https://i.imgur.com/QU5C0JW.png

Creating our Store

This is where we start using Redux. First, we'll need to install required packages, which are:

  • Redux - for state managementst
  • React-Redux - for connecting Redux to our React app

npm install redux react-redux or yarn add redux react-redux

Now in the index.js of our app, we need to create the store object and pass it on to its children components.

First we'll import them to index.js:

import { Provider } from "react-redux";
import { createStore, combineReducers } from "redux";
Enter fullscreen mode Exit fullscreen mode

We'll also add our reducers from inside the reducer folder:

import timerReducer from "./store/reducers/timer";
import lapsReducer from "./store/reducers/laps";
Enter fullscreen mode Exit fullscreen mode

Now, as we have two different reducers so we are going to use the combineReducers function to combine them and make a rootReducer. After which we'll be able to create a store by passing this into createStore function, as so.

const rootReducer = combineReducers({
    tmr: timerReducer,
    lpr: lapsReducer,
});

const store = createStore(rootReducer);
Enter fullscreen mode Exit fullscreen mode

Note: The combineReducers is going to store both timer and lap reducer in two different object properties, viz. tmr and lpr You can name them anything you want.

Lastly, but most important we need to pass the store to all of the children components for them to access it locally. We can do that through, the Provider we included from react-redux package, like this.

ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById("root")
);
Enter fullscreen mode Exit fullscreen mode

You can at times visit the state/redux branch on GitHub to see the code, if you get stuck somewhere.

Creating all Actions

As we've seen in the previous blogs, it's a good practice to assign variables to the type property of the action object rather than providing strings directly, so we're going to create a file called actions.js inside of /store folder to have all the action types. So, lets just do that.

// actions.js

export const INCREMENT = "INCREMENT";
export const DECREMENT = "DECREMENT";
export const COUNTDOWN = "COUNTDOWN";
export const COUNTDOWNATZERO = "COUNTDOWNATZERO";
export const CREATELAP = "CREATELAP";
export const REMOVELAP = "REMOVELAP";
export const RESET = "RESET";
export const RESETLAPS = "RESETLAPS";
Enter fullscreen mode Exit fullscreen mode

So, don't be intimidated by these long variable names, you can keep them as you wish later and also we're going to use them very soon so you'll know which action type does what and why we need them.

Connecting with Timer component

So, finally we are ready to connect with the Timer.js component to our global state. Now, firstly we need to import required variables and functions.

import { connect } from "react-redux";

import {
    INCREMENT,
    DECREMENT,
    COUNTDOWN,
    COUNTDOWNATZERO,
    CREATELAP,
    REMOVELAP,
    RESET,
    RESETLAPS,
} from "../../store/actions";
Enter fullscreen mode Exit fullscreen mode

So, now if you'd look at the code carefully, you'll notice in every function attached to these buttons, there's a this.setState call which mutates our local state and re-renders our component, this means, this is what we have to change by using Redux.

The very next thing we should do, is to come down to export default Timer and wrap the Timer within the connect function we just imported. Like this:

export default connect(mapStateToProps, mapDispatchToProps)(Timer);
Enter fullscreen mode Exit fullscreen mode

Wait, but what are mapStateToProps and mapDispatchToProps? These are just functions we're going to define soon. We'll come back to these once after we're done making our Reducers.

Creating our Reducers

Finally it's time to create our reducers which will pass the updated state to the store object, which will lead our component to re-render and show us the new time. As you've already made two files: timer.js and lap.js , you can jump right in.

Making timer.js Reducer

Firstly, lets import our action variables from above the file structure.

import {
    INCREMENT,
    DECREMENT,
    COUNTDOWN,
    COUNTDOWNATZERO,
    RESET,
} from "../actions";
Enter fullscreen mode Exit fullscreen mode

Now, lets create an initialState which will hold the required state to begin our app with.

const initialState = { time: { h: 0, m: 0, s: 0 }, seconds: 0 };
Enter fullscreen mode Exit fullscreen mode

Alright, now we'll make the reducer function. I suggest you once to go over how the state is being changed (using this.setState) in each of the functions that we passed to the onClick handler of Button component. This will also give you a clear understanding of our reducer function.

With that being said, this is how the reducer will look like:

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case INCREMENT: // on clicking "+"
            return {
                ...state,
                seconds: state.seconds + 60,
                time: action.secToTime(state.seconds + 60),
            };
        case DECREMENT: // on clicking "-"
            return {
                ...state,
                seconds: state.seconds - 60,
                time: action.secToTime(state.seconds - 60),
            };
        case COUNTDOWN: // after clicking "start"
            return {
                ...state,
                seconds: state.seconds - 1,
                time: action.secToTime(state.seconds - 1),
            };
        case COUNTDOWNATZERO: // after clicking "start" but when time becomes 0
            return {
                ...state,
                seconds: 0,
                time: { h: 0, m: 0, s: 0 },
            };
        case RESET: // on clicking "reset"
            return {
                ...state,
                time: { h: 0, m: 0, s: 0 },
                seconds: 0,
            };
        default:
            return state;
    }
};

export default reducer;
Enter fullscreen mode Exit fullscreen mode

One thing you'll notice is, we pass secToTime as a function in our action object for a lot of times, that's because we always need this function to give us the exact time format, by just inputting seconds.

Making laps.js Reducer

Firstly, lets import our action variables from above the file structure.

import { CREATELAP, REMOVELAP, RESETLAPS } from "../actions";
Enter fullscreen mode Exit fullscreen mode

Now, lets create an initialState which will hold the required state to begin our app with.

const initialState = { laps: [] };
Enter fullscreen mode Exit fullscreen mode

Alright, now we'll make the reducer function. I suggest you once to go over how the state is being changed (using this.setState) in each of the functions that we passed to the onClick handler of Button component. This will also give you a clear understanding of our reducer function. Here we go:

const reducer = (state = initialState, action) => {
    switch (action.type) {
        case CREATELAP:
            const newLaps = [...state.laps];
            return {
                ...state,
                laps: newLaps.concat(action.time),
            };
        case REMOVELAP:
            return {
                ...state,
                laps: state.laps.filter((item, index) => index !== action.id),
            };
        case RESETLAPS: {
            return {
                ...state,
                laps: [],
            };
        }
        default:
            return state;
    }
};

export default reducer;
Enter fullscreen mode Exit fullscreen mode

As mentioned, this reducer will just take care of the laps array which fills up when the user clicks on Lap button and also resets on hitting Reset and deletes itself up on being clicked.

Note: The reducer always returns a new state immutably to pass on to the store, if you wanna find out how we can return immutable values check out my previous post.

How to Reducer and Store from Component?

mapStateToProps

This is a function which works under the hood to give us access to the global state in our component, which then can be accessed as props in our component. We can make it like this.

const mapStateToProps = (state) => {
    return {
        time: state.tmr.time,
        seconds: state.tmr.seconds,
        laps: state.lpr.laps,
    };
};
Enter fullscreen mode Exit fullscreen mode

See, how we need to access the tmr and lpr properties from inside the state ? It's because we have combined our two different routers, lap.js and timer.js in our index.js file using combineReducers and we gave these names in our index file, remember? This will get us the right value of our state.

mapDispatchToProps

If you were thinking how shall we pass the actions from our component to the reducer, then perfect. This is what this function does. This returns a bunch of functions inside an object, which when called dispatch the particular action we have written for it. Let me show you our function, here. You'll understand everything soon enough.

const mapDispatchToProps = (dispatch) => {
    return {
        onIncrement: (fn) => dispatch({ type: INCREMENT, secToTime: fn }),
        onDecrement: (fn) => dispatch({ type: DECREMENT, secToTime: fn }),
        onCountDown: (fn) => dispatch({ type: COUNTDOWN, secToTime: fn }),
        onCountDownAtZero: () => dispatch({ type: COUNTDOWNATZERO }),
        onCreateLap: (time) => dispatch({ type: CREATELAP, time: time }),
        onRemoveLap: (id) => dispatch({ type: REMOVELAP, id: id }),
        onReset: () => dispatch({ type: RESET }),
        onResetLaps: () => dispatch({ type: RESETLAPS }),
    };
};
Enter fullscreen mode Exit fullscreen mode

So, now we can access these functions though props in our component and we are going to call them each time we need any state changes.

How to access store from any component?

The function mapStateToProps gives us access to the global store through props.

From, above we can see this function returns three properties, viz. time , seconds and laps . We can access this wherever we want by just doing, this.props.time, this.props.seconds and this.props.laps .

Dispatching Actions instead of using this.setState()

We've already access to all the actions dispatchers and global state in our component through props, by using the mapStateToProps and mapDispatchToProps functions. Now, we just need to replace our this.setState() with dispatching required actions.

For example:

When we click on + there's a this.incTimer function which executes, which is this.

incTimer() {
        if (this.state.seconds >= 0) {
            this.setState((prevState) => ({
                seconds: prevState.seconds + 60,
                time: this.secondsToTime(prevState.seconds + 60),
            }));
                }
}
Enter fullscreen mode Exit fullscreen mode

We need to replace this with calling our action dispatch function: onIncrement which is defined in our mapDispatchToProps function and available through this.props.

Here's our new incTimer function:

incTimer() {
        if (this.props.seconds >= 0) {
            this.props.onIncrement(this.secondsToTime);
        }
}
Enter fullscreen mode Exit fullscreen mode

This does, the exact same thing as we used to do previously, with our local state.

Here's the rest of the click handlers.

decTimer() {
        // Runs only if seconds > 61, to not result in getting -ve values rendered
        if (this.props.seconds > 61) this.props.onDecrement(this.secondsToTime);
    }

    startTimer() {
        // Runs only if timer isn't started already and seconds are atleast more than zero
        if (this.timer === 0 && this.props.seconds > 0) {
            this.timer = setInterval(this.countDown, 1000);
        }
    }

    countDown() {
        // Removing a sec and setting state to re-render
        this.props.onCountDown(this.secondsToTime);

        // Check if we're at zero
        if (this.props.seconds === 0) {
            clearInterval(this.timer);
            this.props.onCountDownAtZero();
        }
    }

    stopTimer() {
        // Stop only if timer is running and seconds aren't zero already
        if (this.timer !== 0 && this.props.seconds !== 0) {
            clearInterval(this.timer);
            this.timer = 0;
        }
    }

    lapTimer() {
        // Lap only if timer is running and seconds aren't zero already
        if (this.timer !== 0 && this.props.seconds !== 0)
            this.props.onCreateLap(this.props.time);
    }

    resetTimer() {
        // Getting back state to its original form
        this.props.onReset();
        this.props.onResetLaps();

        // Also, if timer is running, we've to stop it too
        if (this.timer !== 0) {
            clearInterval(this.timer);fn
            this.timer = 0;
        }
    }
Enter fullscreen mode Exit fullscreen mode

This will now set up our actions to dispatch whenever the user clicks any of the buttons, which will take it to the reducer and after updating the state object, it'll pass onto the global store and return to us the updated state.

Render the Timer Component

Now, what about the render() lifecycle method? This also needs to have access to our local state in order to display the current timer, using this.timeFormatter. And, also display the laps and make it vanish when we click over them.

So, we need to replace the below code from our render() method to have access to the store directly, instead of calling this.state.

let { h, m, s } = this.timeFormatter(this.state.time);

let laps = null;

if (this.state.laps.length !== 0) {
    laps = this.state.laps.map((lap, id) => {
        let { h, m, s } = this.timeFormatter(lap);
        return (
            <Label
                key={id}
                clicked={() => this.removeLap(id)}
                lapTime={`${h}:${m}:${s}`}
            />
        );
    });
}
Enter fullscreen mode Exit fullscreen mode

Do you remember how are we supposed to access our store?

As we've already mapped our state to props, we can easily access them like this.

  • this.props.time
  • this.props.laps
  • this.props.seconds

Let's do just that.

let { h, m, s } = this.timeFormatter(this.props.time);

let laps = null;

if (this.props.laps.length !== 0) {
    laps = this.props.laps.map((lap, id) => {
        let { h, m, s } = this.timeFormatter(lap);
        return (
            <Label
                key={id}
                clicked={() => this.props.onRemoveLap(id)}
                lapTime={`${h}:${m}:${s}`}
            />
        );
    });
}
Enter fullscreen mode Exit fullscreen mode

Now we can easily display data from our global store in our render() method, which makes our app work as a charm. You can now, run your server using npm run start or yarn start to see how your countdown timer works. I hope this was fun building.

Conclusion

I've been using Redux a lot lately, not for huge projects though, but in lot of my side projects and it has been awesome learning this. I know you can feel intimidating at first, but trust me when you're a week or so into it this all start to seem familiar and you are soon enough confident to carry on your learning journey! I'll keep you posted with what I'm learning next! Keep building! 💪

Follow my journey and get more updates on what I'm upto, @heytulsiprasad.

Top comments (0)