For more than half of 2019, I worked on an open-source Gatsby project called Medatime using React.js.

In this article, I’m going to focus on the button controls: Navigation and Play/Pause buttons. Breaking these down and showing the data path is a lesson in state management and how to balance between using Redux, Hooks and the Context API.

NavButton

Here’s the first iteration of the navigation button control to render the timer state.

function NavButton(props) {
	const handleClick = () => {
		const { transitionCallback } = props
		transitionCallback()
	}

	return(
		<button
			style={{padding:'1rem', backgroundColor:'white'}}
			onClick={handleClick}
		>
			Switch Step
		</button>
	)
}

View full code snippet on Github

NavButton is a functional component that has a simple purpose: a button that “navigates” from the initial state to the timer state, allowing desired meditation time to pass to become timer countdown digits.

TimerScreen imports NavButton as a component set with an event handler eventually invoking setState to navigate to the timer state.

class TimerScreen extends React.Component {
	{/* 
	  * Component state and methods declared
	  */}

	toggleTimerScreen() {
		const { isTimerVisible } = this.state

		this.setState({ isTimerVisible: !isTimerVisible })
	}

	render() {
		return (
			<div className="TimerScreen">
		        <NavButton transitionCallback={this.toggleTimerScreen.bind(this)} />
			</div>
		)
	}
}

View full code snippet on Github

By setting isTimerVisible from false to true, the timer shows itself and text field hides itself, triggered in a couple other methods in the component. Pretty simple.

Play/Pause button

The Play button control has various states that needed to be accounted for, such as what to do with the countdown digits as you play and pause from various other states.

Just like NavButton, TimerControlButton‘s first iteration started out as a simple class component.

class TimerControlButton extends React.Component {
	{/* 
	  * Component state and methods declared
	  */}

  handleClick(event) {
    const { isPlaying } = this.state
    const { playCallback, pauseCallback } = this.props

    if (isPlaying)
      playCallback()
    else
      pauseCallback()

    this.setState({ isPlaying: !isPlaying })
  }

  render() {
    return (
      <button onClick={this.handleClick.bind(this)}>
        {this.state.isPlaying ? `Play` : `Pause`}
      </button>
    )
  }
}

View full code snippet on Github

Does this look familiar? TimerControlButton and NavButton are performing similar actions of inverting state variable booleans, from false to true. But TimerControlButton gets quite a bit more complicated.

When the timer is started, TimerControlButton‘s button element has an event listener that will start and stop the timer, triggering props playCallback() and pauseCallback() to invoke a function in the Timer component. The timer start and stop methods are logically called startClock() and stopClock().

To keep this snippet simple, I’m gonna show a high-level view of what’s happening.

class Timer extends React.Component {
	{/* 
	  * Component state and methods declared
	  */}

  returnStopClock = (callback, context, timerId) => {
    return callback.bind(context, timerId)
  }

  startClock() {
    this.stopClockCallback = this.returnStopClock(clearInterval, window, setInterval(() => {
      this.setState(
        /* Determine total seconds */, 
        /* Use current seconds to run down timer or stop the clock */
      )
    }, 1000))
  }

  stopClock() {
    this.stopClockCallback()
  }
}

View full code snippet on Github

It’s a mouthful; just click above the full code snippet link. The timer’s state changes started and ended with startClock(). The countdown either started or stopped through this complicated function, where it did things like get the current seconds, get a time string, setting the state of the digits as well as setting the state of time itself and stop the clock. So how did we improve this?

Redux to the rescue?

A growing problem to solve was dealing with state. As you can see in the various component snippets above, state is changed in many components often passed down as props to child components. When state starts being passed down several levels deep, it’s time to reconsider state management.

As this project grew in scope, so did advancements in React. The initial idea of state management was to introduce Redux. At the beginning of this project, however, the React team dropped a new feature called Hooks. After a little time to understand what Hooks provide, we settled on a mix of the useReducer and useContext + Redux. This provided the opportunity to learn Hooks and Redux and how they work with each other. Some state moved back to the top and was put into global storage. Using Redux-inspired actions and reducers, the Context and Hooks APIs can access state from any component.

With both a Play/Pause button and a Restart button able to control the timer, and with similar logic for both buttons, it makes sense to use a global store of actions. Maybe a new Skip button will be introduced later. Redux and Hooks allows for efficient iteration. So let’s take a look at what’s going on.

Button Controls State Management

Here’s the Play button component.

<TimerControlButton
  type={controlTypes.play}
  isPlaying={isPlaying}
  onClickCallback={onPlayButtonClick}
/>

View full code snippet on Github

As we can see, there are three things to account for.

  1. The type prop is defining the type of button, Play button here, defined in TimerControlButton.
  2. isPlaying starts the fun. State is updated with useState() instead of setState() since TimerControls is now a functional component. Inverting the boolean, same as before.
  3. onPlayButtonClick gets us to the power of Hooks and Redux. It eventually flows to the top of the app to use a reducer to apply the correct action, which in this case is to pause the timer.

So, let’s follow the code all the way up to top.

From ‘click’ to Pause

Using TimerControlButton, onClickCallback prop is listening to run onPlayButtonClick, which determine if the timer is playing…

  const onPlayButtonClick = () => {
    if (isPlaying)
      stopClockCallback()
    else
      playCallback()
  }

View full code snippet on Github

…and in this case, onPlayButtonClick invokes stopClockCallback for pausing.

  const stopClockCallback = () => {
    setIsPlaying(false)
    stopClock(
      timerState.stopClockCallback,
      dispatch
    )
  }

View full code snippet on Github

See that reference to stopClock? This method is coming from the Timer component. Funny thing, though, we don’t see a direct reference to stopClock.

import React from 'react'
	{/* 
	  * import and export statements
	  */}

const TimerProvider = ({ children }) => {
	{/* 
	  * Component state and methods declared
	  */}

  return (
    <TimerContext.Provider
      value={{ state, dispatch }}
    >
      {children}
    </TimerContext.Provider>
  )
}

export default TimerProvider
export * from './actions'

View full code snippet on Github

Where is it? The line to focus on is the last one, export * from './actions'. This is called a re-export. It’s a way to add all of actions.js exports to the current component. This means that in addition to the default export for TimerProvider, this file is magically showing a handful more exports set in the actions file.

Finally, actions.js is where we find stopClock.

export const stopClock = (prevClearInterval, dispatch) => {
  if (prevClearInterval) prevClearInterval()

  dispatch(setStopClockCallback(null))
}

View full code snippet on Github

Well that was quite the journey! We see that this function has a couple of parameters. The first parameter is to use clearInterval to stop the clock at its given state, which as we see above is timerState.stopClockCallback. The second one is the dispatch, used to set the desired action type which is set within setStopClockCallback().

function setStopClockCallback(callback) {
  return {
    type: SET_STOP_CLOCK_CALLBACK,
    callback,
  }
}

View full code snippet on Github

Now with the type SET_STOP_CLOCK_CALLBACK, the reducer knows what to do. But where does the reducer come into this? Dispatch!

Using the Context API, state and dispatch are passed down from the timer component in the provider as values. Within the TimerControls component and using useContext hook, the state and dispatch are provided in the local component directly, rather than as props.

const { state: timerState, dispatch } = useContext(TimerContext)

View full code snippet on Github

Now we can circle back to stopClockCallback and get that dispatch run where dispatch is a property of the useReducer() function. This data path is where Redux and useReducer() compare. Similar to a Redux store, useReducer() stores state default values.

import timerReducer from './reducer'

export const TimerContext = React.createContext(null)

const TimerProvider = ({ children }) => {
  const [state, dispatch] = useReducer(timerReducer, {
    /* state variables defined */
  })

  return (
    <TimerContext.Provider
      value={{ state, dispatch }}
    >
      {children}
    </TimerContext.Provider>
  )
}

View full code snippet on Github

We can see that timerReducer is referred within useReducer(), and this goes into a Redux-inspired reducer file with logic to stop the timer.

const timerReducer = (state, action) => {
  switch (action.type) {
    case SET_STOP_CLOCK_CALLBACK:
      return { ...state, stopClockCallback: action.callback }
  /* other cases */
  }

View full code snippet on Github

Now with this switch statement, the state is updated, the timer is finally paused and the state changes. Phew!

From ‘click’ to Play

The biggest difference between Pause and Play is what happens in the actions file. In the TimerControls component, onPlayButtonClick sees that isPlaying is true instead of false, so it calls playCallback(). It follows the same path up to the actions file where we see a different function called startClock.

export const startClock = (prevClearInterval, dispatch) => {
  if (prevClearInterval) prevClearInterval()

  dispatch(
    setStopClockCallback(
      returnStopClock(
        clearInterval,
        window,
        setInterval(() => {
          dispatch(decrementDigitsState())
        }, 1000)
      )
    )
  )
}

View full code snippet on Github

The dispatch being set here is far more complex than stopClock, since we’re calling multiple functions inside of other functions. But at the center of this is returnStopClock(), which has three arguments that all set global functions to clear a running interval, look to the window scope and set a new interval that dispatches another function which sets the type to continue the timer.

Review

If you made it all the way down here, thank you for following this adventure! This was a multi-hop path up and down the React component tree with twists and turns in the form of hooks, exports, imports, and hierarchy. The amount of research it takes to explain all of this is a lesson itself. But to truly learn how each part works in an app is the master what you know.