What is the best way to stop the date updating in react?

Viewed 938

I am mapping through an array of objects and displaying their values in a card, each object has a title, comment, and datePosted field. The datePosted field is put throught a function I made to display it on the card as 'Posted 2 hours ago' or 'Posted '2 minutes ago'. This is updating though everytime the state changes, which isn't a problem when it was posted days ago, or even hours, as nothing changes, but when a new post is first uploaded and saved, the date being displayed updates practically everytime you type a key, as it goes from 'Posted 1 second ago' to 'Posted 2 seconds ago', to 'Posted 3 seconds ago' and so on... . Is there anything I can do to get around this, I've considered saving the component in a different state and displaying that, but I don't think that will work as the state needs to be updated everytime the user changes a title or comment so it can be displayed in real time.

Here's an example of the JSX that is displaying the card in case it helps. timeCalc is the function that calculates how long ago the post was created

{posts.map(item => (
  <div>
    <div>
      <h1>{item.title}</h1>
      <h3>{item.comment}</h3>
    </div>
    <div>
      <h3>{timeCalc(item.datePosted)}</h3>
    </div>
  </div>
))}
3 Answers

I would extract the code to its own component, and then that component can use one of several possible techniques to remember the value from its first render. Here's one with useMemo:

{posts.map(item => <Post item={item}/>)}

//... elsewhere:
const Post = ({ item }) => {
  const time = useMemo(() => {
    return timeCalc(item.datePosted);
  }, []);

  return (
    <div>
      <div>
        <h1>{item.title}</h1>
        <h3>{item.comment}</h3>
      </div>
      <div>
        <h3>{time}</h3>
      </div>
    </div>
  );
}

Other possible ways to remember the initial value are with state:

const Post = ({ item }) => {
  // Note that i have no need for `setTime`, as this value will never be changed
  const [time] = useState(timeCalc(item.datePosted));

Or with a ref:

const Post = ({ item }) => {
  const timeRef = useRef(timeCalc(item.datePosted));

  // ...
  <h3>{timeRef.current}</h3>

You can create a component and memoize it using React.memo and areEqual function to avoid re-rendering when props i.e. date are same.

Demo:

const inputDate = new Date().toISOString();

function timeCalc(date) {
  return moment(date).fromNow()
}
function TimeCalc({date}) {
  return <h4>{timeCalc(date)}</h4>
}
function areEqual(prevProps, nextProps) {
  return prevProps.date === nextProps.date
}
const TimeCalcMemo = React.memo(TimeCalc, areEqual);

function App() {
  const [v, sv] = React.useState('')
  return (<div>
    <div>{v}</div>
    <div>Enter something after a minute</div>
    <input type="text" value={v} onChange={e => sv(e.target.value)} />
    <TimeCalcMemo date={inputDate}/> memoized
    <TimeCalc date={inputDate}/> not memoized
  </div>)
}

ReactDOM.render(<App />, document.getElementById('mydiv'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js"></script>
<script crossorigin src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<body>
<div id="mydiv"></div>
</body>

You can also try conditional rendering.

const lastUpdated = timeCalc(item.datePosted)

{posts.map(item => (
  <div>
    <div>
      <h1>{item.title}</h1>
      <h3>{item.comment}</h3>
    </div>
    <div>
      {
          (compare timestamp here using moment.js or anyway you prefer) &&
          <h3>{timeCalc(item.datePosted)}</h3>
      }
    </div>
  </div>
))}
Related