When developing React applications, it is common to create reusable components that can be used in multiple places by passing appropriate props to achieve the desired behavior. However, there may be situations where you want to control the state of a component from outside of its scope, such as when you need to manage the child component's state from the parent component.
This blog explains how to control the state of the child component using one of the hooks(secret will be revealed soon :)) ) offered by React.
We are going to build a Timer component that can be used across the project!.
And it should have a start, stop and reset functionality
let's create our timer component,
Let's breakdown the timer
currentTime: we are storing the initial state of the timer in the currentTime variable
In our example, the timer and setTimer variables are created using the useState hook, which allows us to initialize and manage the state in our Timer component. The useState hook returns an array with two elements: the current state value (timer) and a function to update the state (setTimer). which is destructed and stored respectively.
The currentTime variable is passed as the initial state value to useState. It holds the initial state that will be assigned to the timer variable when the component is first rendered. The setTimer function can later be used to update the value of the timer as needed.
Now, the state is created, it's time to update the state using the handlers
We require three handlers to update the state namely,
start(): To start the timer,
stop(): To pause the timer,
reset(): To reset it to the initial state
so in our example, we have created these handlers and will go through it one-by-one
1. start() This method contains the core logic of this timer component, which can be described as follows,
We create a setInterval method to implement the timer functionality. The setInterval is a web API offered by the browser environment that allows us to repeatedly execute a callback function at a specified time interval. In this case, we set the callback to be executed every 1000ms, which is equivalent to 1 second
as we created an outline of the timer logic using setInterval let's take a closer look at the callback function that is responsible for updating the state based on the logic executed for each second
instead of directly updating the state, we first get the prev state using the setTimer function, based on the prev state we update the current state
let { hours, minutes, seconds} = prev, this line destructs each property from the previous state and initializes the values respectively
then we check for the seconds value to be equal to 59 if it is then we increment the minutes from the previous(prev) value and override the seconds value to 0 to keep track of it for the next minutes and so on
then the second if condition that is nested within this block is to check for the minutes whether it has reached 60 because every 60 seconds update every minute, which increases the hours too, if the minutes have reached 60 seconds we override the minutes value to 0 and increments the hours to 1 from the previous(prev) state,
then we terminate the if condition by returning the new state with the updated value based on the conditions.
return {hours, minutes, seconds} ( es6 syntax 'object literals' )
Before checking whether the seconds have reached 59 or not, we increment the seconds within the callback function for each execution by default returning the state as {...prev, seconds: prev.seconds+1} with the previous state using the rest operator.
and we store the created setInterval instance/id in a variable called myTimerReference to clear it whenever needed.
2. stop() This method simply clears the interval that is stored in the myTimerReference variable, so that the state is not updated as the setInterval which has the state update logic won't be executed because it has been destroyed.
Note: here we are not updating the timer to the initial state, because by the next time the start() function is executed the new timer reference is created and stored in myTimerReference but now it updates the state from a previous state, not from the initial state, by doing so we achieved the pausing functionality.
3. reset() This method clears the stored interval to stop the timer by calling the stop() method and additionally updated the state to the initial state setTimer(currentTime) so that by the next time the start method is called the timer starts from 00 : 00 : 00 hours.
So, this is how we use our Timer component . . .
A new requirement is added, where the same timer functionality should be implemented using different layouts on different screens, but there should be no change in the layout that was previously created.
ha.... the nightmare for a developer starts ...
so now we go and create a new timer component with the same functionality but with a different layout, but we have already created a Timer component that servers the same purpose, but it can't be used as the state is controlled by itself.
but being an skilled developer now we are going to modify the Timer component in such a way that the state can be controlled from the parent component.
so the approach is,
Now some how we have to give access to the parent component to control the state, Secret revealed! . . .
Here we make use of the useRef() hook to achieve this
How?
Let's understand this with the modified Timer component
if we observe carefully the Timer component is exported by wrapping it with a function called forwardRef(Timer)
Because the reference from the parent cannot be passed as an property of props to the functional component, so to pass the ref as an props to the child component we wrap the Timer component in forwardRef() which has been offered by react allows the component accept the ref as an second parameter.
Now we got the ref of the parent and we attach that with the div
<div ref={ref}> . . . </div>
but is that enought to do the work ? a big nooo.......
The key thing here is to attach the handlers such as start, stop, reset that updates the state to the ref as below
It's time to modify our App component
Here the parent component App(I) want the reference ! of the one of its child component by passing the timerRef as an ref to the Timer component and control the state by using it in the onClick of the respective events.
Ha this way we control the Timer
Conclusion
In addition to using ref, it's worth noting that the desired functionality can also be achieved by leveraging the power of Higher Order Components (HOC). By creating a custom HOC and wrapping the component, we can add the necessary logic and functionality. This approach offers flexibility in reusing and composing functionality across multiple components, promoting code reuse and separation of concerns. Therefore, both ref and HOC are viable options to accomplish the desired behavior, and the choice between them depends on specific project requirements and preferences.
If you believe an example using Higher Order Components (HOC) would be helpful, please let me know in the comments.