1

I have a model that has expiredAt property:

type Expirable {
   expiredAt: Date;
}

The component (idea) is basically like the following:

const [model, setModel] = useState<Expirable>({
  expiredAt: new Date((new Date).getTime() + 20000)
});

return (
  { expired(model) ? (
     <Text>Expired</Text>
  ) : (
     <Text>Not Expired</Text>
  )}
);

How can I implement expired function (or React Hook) to update component at due time?

Please notice that multiple components are rendered with models fetched from API, so hard-coding won't be a solution here.

glinda93
  • 7,659
  • 5
  • 40
  • 78

4 Answers4

3

I suggest you to define a custom hook (to simplify your component code and future reuse):

import React, { useEffect, useState, useRef } from "react";
import "./styles.css";

const useExpired = (time)=>{
  const [expired, setExpired] = useState(false);
  const timoutRef = useRef();
  useEffect(()=>{
    timoutRef.current = setTimeout(()=>{
      setExpired(true);
    }, time);
    return ()=>{
      clearTimeout(timoutRef.current);
    }
  },[time]);
  return expired;
}

export default function App() {
  const [model, setModel] = useState({
    expiredAt: new Date().getTime() + 5000,
  });

  const expired = useExpired(model.expiredAt - new Date().getTime());

  return <div>{expired ? <h1>Expired</h1> : <h1>Not expired</h1>}</div>;
}
Max
  • 1,020
  • 7
  • 13
  • 2
    You don't need to store the time out ref using useRef. Just use `const timoutRef = setTimeout(...` inside your useEffect – neiker Oct 27 '20 at 12:21
2

Live demo here

import React, { useEffect, useState } from "react";
import "./styles.css";

export default function App() {
  const [model, setModel] = useState({
    expiredAt: new Date().getTime() + 5000,
    expired: false
  });

  const expired = m => {
    return m.expired === true;
  };

  useEffect(() => {
    setTimeout(() => {
      setModel(currentModel => ({ ...currentModel, expired: true }));
    }, model.expiredAt - new Date().getTime());
  }, []);

  return <div>{expired(model) ? <h1>Expired</h1> : <h1>Not expired</h1>}</div>;
}
Tony Nguyen
  • 3,298
  • 11
  • 19
fishstick
  • 2,144
  • 1
  • 19
  • 14
  • You are not using useEffect correctly. Send an empty array to second parameter. – glinda93 Jul 02 '20 at 21:01
  • @bravemaster thanks for bring that up, updated. No idea why every time when I add the empty array, eslint gives me a warning. Same on codesandbox. – fishstick Jul 02 '20 at 21:17
  • 2
    Use `// eslint-disable-line react-hooks/exhaustive-deps`. Since setTimeout function needs to be called just once, we don't need model dependency. For more info, see this post: https://stackoverflow.com/questions/55840294/how-to-fix-missing-dependency-warning-when-using-useeffect-react-hook – glinda93 Jul 02 '20 at 21:22
  • Work like a charm! Thanks! (Updated in codesandbox) – fishstick Jul 02 '20 at 21:32
  • 1
    directly access `model` in `setTimeout` might cause `model` is not updated problem: Check here: https://reactjs.org/docs/hooks-faq.html#what-can-i-do-if-my-effect-dependencies-change-too-often – Tony Nguyen Jul 05 '20 at 04:50
  • @TonyNguyen Thanks for the heads up! I've approved your edit. – fishstick Jul 05 '20 at 06:21
2

You can add expired property for model and set set it to true when the time coms

const [model, setModel] = useState<Expirable>({
  expiredAt: new Date((new Date).getTime() + 20000)
  expired: false
});
// You can also use useRef instead of useState here.
const [expirationTimeoutId] = useState(() =>{
  return setTimeout(() =>{
     // setModel on currentModel to have last value of model
     setModel(currentModel => ({...currentModel, expired: true }))
  }, model.expiredAt - Date.now())
});
// You should clear timeout when the component is unmounted
useEffect(() =>{
   return () => clearTimeout(expirationTimeoutId)
},[])

return (
  { model.expired ? (
     <Text>Expired</Text>
  ) : (
     <Text>Not Expired</Text>
  )}
);

Updated: If you don't want to touch model you can do this

const [model, setModel] = useState<Expirable>({
  expiredAt: new Date((new Date).getTime() + 20000)
});

const [isModelExpired, setIsModelExpired] = useState(false)

const [expirationTimeoutId] = useState(() =>{
  return setTimeout(() =>{
    setIsModelExpired(true)
  }, model.expiredAt - Date.now())
});
// You should clear timeout when the component is unmounted
useEffect(() =>{
   return () => clearTimeout(expirationTimeoutId)
},[])

return (
  { isModelExpired ? (
     <Text>Expired</Text>
  ) : (
     <Text>Not Expired</Text>
  )}
);
Tony Nguyen
  • 3,298
  • 11
  • 19
1

Apart you said

Please notice that multiple components are rendered with models fetched from API, so hard-coding won't be a solution here.

but in your example you used an hard-coded expiration time rather than showing us how the model is provided to the component once fetched from API; I'll assume the model is provided to the component as a prop.

Once said that, the purpose of React Hooks is to make the code shorter (better maintainable) but this is true only when the component has to do a really few things. This is a classical example case where using a class component makes the code much more short, clean and maintainable.

class ModelComponent extends Component {
  constructor(props) {
    super(props);

    const elapsed = props.model.expiredAt - new Date().getTime();

    this.state = { expired: elapsed <= 0 };
    if(! this.state.expired) // Set the timeout only if the model isn't already expired
      this.state.to = setTimeout(() => this.setState({ expired: true, to: null }), elapsed);
  }

  componentWillUnmount() {
    if(this.state.to) clearTimeout(this.state.to);
  }

  render() {
    return this.state.expired ? (
      <Text>Expired</Text>
    ) : (
      <Text>Not Expired</Text>
    );
  }
}

Is this code more clear or more complicated than the possible solutions using React Hooks?

Hope this helps.

Daniele Ricci
  • 15,422
  • 1
  • 27
  • 55
  • Models are fetched from API, `expiredAt` too for that matter. I just wanted to show an example there. – glinda93 Jul 05 '20 at 13:34
  • My codebase is entirely React hooks. Is there any way I can combine React hooks with react component, if I'd want to use your answer? Or is mixing React Hooks with React class components even a good approach? – glinda93 Jul 05 '20 at 13:35
  • There is absolutely no problem mixing them; I usually write main components as class components (since they are more powerful) and small satellite components as function components... using hooks when required. – Daniele Ricci Jul 05 '20 at 13:49