replacing componentDidUpdate with useEffect

Viewed 2148

I'm replacing lifecycle methods in class based component into useEffecthooks since I'm into functional component. I'm not sure I'm using useEffect properly..

we can't point previous state or props in useEffect, how can I deal with these with useEffect?

Let's say we have props named src, and I want to check if this src has changed, If not, there would be some logic for it.

componentDidUpdate(prevProps) {
  const { src } = this.props; 

if(prevProps.src !== src) {
   this.cleanUp(); 
   this.init(); 
} else{
   // todo something 
}
}

how can I deal with this using useEffect?

2 Answers

Think of useEffect has a "hook" in this case, that relies on something to trigger it when there's a change.

If you give useEffect nothing, it will just be triggered once on load. This is the equivalent to componentDidMount.

If you give it multiple object or elements to follow it will trigger based on the changes of those elements.

TL;DR:

const App = props => {
   useEffect(() => {
      // your code
   }, [props.yourPropertyToWatch]);
}

UPDATE

Based on @hmr's feedback, it would be ideal to add the clean up as well, which is the equivalent of componentWillUnmount.

const App = props => {
   useEffect(() => {
      // your code

      // once done with the component
      return () => {
         // your unmount code
      }
   }, [props.yourPropertyToWatch]);
}

You can use it like this:

function App(props) {
  const { src } = props;
  React.useEffect(() => {
    init();
    return () => cleanup();
  }, [src]);

If cleanup and init aren't static functions and need to access local state or props then I need to see more code and probably have to use useCallback on init and cleanup so you can make them dependencies of the effect to avoid stale closures or infinite renders.

Here is an example of external init function using useCallback:

//message channel has getAll, sendMessage and addListener
//  this is just an example and added to demonstrate hooks
const messageChannel = (() => {
  //function creating id to give each message unique id
  const id = ((id) => () => id++)(1);
  let listeners = [];
  const data = {};
  const addListener = (channel, callback) => {
    listeners.push({ channel, callback });
    //return a function that when called will remove
    //  listener
    return () =>
      (listeners = listeners.filter(
        ({ callback: fn }) => callback !== fn
      ));
  };
  const dispatch = (channel, newData) =>
    listeners
      .filter(({ channel: c }) => c === channel)
      .forEach(({ callback }) => callback(newData));
  const sendMessage = (channel, message) => {
    if (!data[channel]) {
      data[channel] = [];
    }
    const newMessage = { id: id(), message };
    data[channel] = [newMessage, ...data[channel]];
    dispatch(channel, newMessage);
  };
  const getAll = (channel) => data[channel] || [];
  return {
    addListener,
    sendMessage,
    getAll,
  };
})(); //IIFE creating message channel

function Messages({ channel }) {
  //local messages state that holds messages for this channel
  const [messages, setMessages] = React.useState([]);
  //using useCallback so init only changes when channel changes
  const init = React.useCallback(
    //set local messages to all messages of this channel
    () => setMessages(messageChannel.getAll(channel)),
    [channel]//re create init when channel changes
  );
  React.useEffect(() => {
    //call init to set all messages of current channel
    init();
    //add listener for new messages on current channel
    //  addListener returns a function that will remove
    //  the listener when called
    const cleanup = messageChannel.addListener(
      channel,
      //handler for new messages will add new message
      //  to local list of messages
      (newMessage) =>
        //do not mutate state, create a new messages array
        setMessages((messages) => [newMessage, ...messages])
    );
    //return the cleanup function, when channel changes
    //  react will call it and it will remove listener
    //  for previous channel
    return cleanup;
  }, [channel, init]);
  //both channel and init are a dependency but init
  //  only changes when channel changes, it will still
  //  function when removing init from the dependencies
  //  but the linter will complain
  return (
    <div>
      <h3>channel: {channel}</h3>
      <ul>
        {messages.map(({ message, id }) => (
          <li key={id}>{message}</li>
        ))}
      </ul>
    </div>
  );
}
function App() {
  const [channel, setChannel] = React.useState('A');
  const [message, setMessage] = React.useState('');
  const sendMessage = () =>
    messageChannel.sendMessage(channel, message);
  return (
    <div>
      <div>
        <label>
          Channel
          <select
            onChange={(e) => setChannel(e.target.value)}
          >
            <option value="A">A</option>
            <option value="B">B</option>
          </select>
        </label>
      </div>
      <div>
        <input
          type="text"
          value={message}
          onChange={(e) => setMessage(e.target.value)}
        />
        <button onClick={sendMessage}>send message</button>
      </div>
      <Messages channel={channel} />
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>


<div id="root"></div>

Related