Difficulty with Context API [REACT]

Asked

Viewed 29 times

1

I need a timer component to be reflected in another distant component. To do so, I created a context to save the timers states created, since the later timer (depends on the user’s action) should start synchronized with the source timer.

I have tried several approaches but the derived timer always starts in the standard 5 minute state. It’s like every time I create a timer component React recreates the context/ state instance and it doesn’t make any sense.

In short, the context is responsible for iterating each added timer and setting the changes. The timer component is reflected using parentId.

Follows the code:

TIMER

import React, { useEffect } from 'react';
import { makeStyles } from '@material-ui/core';
import { TimersContext, useTimers } from './context/timer';

import AccessAlarmIcon from '@material-ui/icons/AccessAlarm';

import styles from './styles';
import clsx from 'clsx';


const Timer = ({ parentId, updater, card }) => {
    
    const classes = makeStyles(styles)();
    const { handleNewTimer, timers } = useTimers();
    const timer = timers.find(t => t.parentId === parentId);

    useEffect(() => {
        handleNewTimer(parentId, updater);
    });

    return (
        <div className={clsx(classes.container, card && classes.cardStyle)}>
            {
                card && <span style={{ marginBottom: '3px' }}>
                    Atualiza em
                </span> 
            }
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                <AccessAlarmIcon style={{ marginRight: '3px' }} />
                0{timer?.minutes}:{timer?.seconds < 10 ?  `0${timer?.seconds}` : timer?.seconds}s    
            </div>
        </div>
    )
    
}

export default Timer;

CONTEXT

import React, { useState, createContext, useContext, useEffect } from 'react';

export const TimersContext = createContext();

export default function TimersProvider({ children }) {
        
    const [timers, setTimers] = useState([]);

    useEffect(() => {
        let myInterval = setTimeout(() => {
            for (var i=0; i < timers.length; i++) {
                let timer = timers[i];
                if (timer.seconds === 0) {
                    if (timer.minutes === 0) {
                        timer.updater();
                        return;
                    }
                    timer.selfUpdate(timer); 
                    setTimers([...timers]); 
                } else {
                    timer.setSeconds(timer);
                    setTimers([...timers]); 
                }
            }
        }, 1000)

        return () => clearTimeout(myInterval);      
    }, [timers])

    const handleNewTimer = (parentId, updater) => {
        let timerExists = timers.find(t => t.parentId === parentId);
        if (!timerExists) {
            const newTimer = Object.create({
                minutes: 5,
                seconds: 0,
                setSeconds: (timer) => {
                    timer.seconds = timer.seconds -1; // use this                   
                },
                selfUpdate: (timer) => {
                    timer.seconds = 59;
                    timer.minutes = timer.minutes -1; // use this                    
                },
                updater,
                parentId
            })
            setTimers([...timers.filter(t => t.parentId !== newTimer.parentId), newTimer]); 
        } else {
            console.log('Timer is already running.', timers);
        }
    }

    
    return (
        <TimersContext.Provider value={{ timers, handleNewTimer }}>
            {children}
        </TimersContext.Provider>
    )
}

export function useTimers() {
    const context = useContext(TimersContext);

    return context;
}

1 answer

0

Using the same context and parentId, this implementation should work.

To use the same context you must have only one Timersprovider around all Timers. Having the same context, every time you add a new Timer with the same parentId as some previous one it will start counting with the same time as the previous one. Every time you add a Timer with parentId different from others, it will start in 5 minutes.

I assembled a component using the components you created:

export default (props) => {
  const [parentIds, setParentIds] = React.useState(["first"]);

  return (
    <TimersProvider>
      <div style={{ display: "flex", justifyContent: "space-around" }}>
        {parentIds.map((parentId, idx) => (
          <Timer
            key={`${parentId}-${idx}`}
            parentId={parentId}
            updater={console.log}
          />
        ))}
      </div>

      <button
        onClick={() => setParentIds([...parentIds, random(0, 3000).toString()])}
      >
        Adicionar random
      </button>
      <button onClick={() => setParentIds([...parentIds, parentIds[0]])}>
        Adicionar Cópia do primeiro
      </button>
    </TimersProvider>
  );
};

Whenever I click the first button, add a timer with random parentId, it adds a timer that starts in 5 minutes.

Whenever I click the second button, add copy of the first one, it adds a new timer with the same parentId as the first one, thus starting at the same time as the first one is.

The only change I point out is to add the second useEffect Parameter inside the timer because you just want it to register this Timer as soon as it is first rendered not every time it is rendered.

useEffect(() => {
    handleNewTimer(parentId, updater);
  }, []);

Sandbox using your code and this response component

Browser other questions tagged

You are not signed in. Login or sign up in order to post.