Andrei Cristian
Andrei Cristian

Reputation: 93

How can I update a hook from one component and have it update another component as a result?

I'm trying to create a hook with a state and use that as the common source for multiple components.

What I've tried:

import React, {useState} from 'react';

/**
 * Example of Hook that holds state, which should update other components when it changes
 */
const useExternalHookAsState = () => {
  const [message, setMessage] = useState('nothing yet');

  const updateMessage = (newMessage) => {
    console.log('message should be update to..', newMessage);
    setMessage(newMessage);
  }

  return [message, updateMessage];
};

/**
 * Example component for updating the state
 */
const MessageUpdater = () => 
{
  const [message, updateMessage] = useExternalHookAsState();

  return (
    <div>
      <input type="text" value={message} onChange={(e) => updateMessage(e.target.value)} />
    </div>
  );
};

/**
 * Expecting the message to be updated in here as well when the updateMessage is triggered, but that doens't happen
 */
const App = () => 
{
  const [message] = useExternalHookAsState();

  useEffect(()=> {
      console.log('effect on message tirggered');
  }, [message]);

  return (
    <div>
        <p>message is.. {message}</p>
        <MessageUpdater />
    </div>
  );
};

export default App;

Expected behavior: I was expecting that the new {message} will be updated in the App component but that just stays the same as 'nothing yet', and useEffect on the message doens't get triggered.

Am I using hooks in a wrong way? How can I achieve this behavior?

Upvotes: 2

Views: 4610

Answers (2)

Andrei Cristian
Andrei Cristian

Reputation: 93

Though the answer from xom9ikk is right (calling a hook twice will result in each hook having a different state, so one hook can't hold a global state as I've assumed), I have chosen to use the context approach as this doesn't require passing props and results in more clean code when working with a larger codebase.

The end example result that I've used is:

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

/**
 * Initializing Context
 */
const MessageContext = React.createContext();

/**
 * Creating provider with default state
 * - holds the state for the message used everywhere in the App
 * - takes children parameter because it needs to render the children of the context 
 * - updateMessage can be used from any child of provider and will update the global state 
 */
const MessageProvider = ({children}) => {
  const [message, setMessage] = useState('nothing yet');

  const updateMessage = (newMessage) => {
    setMessage(newMessage);
  }

  return (
    <MessageContext.Provider value={[message, updateMessage]}>
      {children}
    </MessageContext.Provider>
  )
}

/**
 * Example component for updating the state
 */
const MessageUpdater = () => 
{
  const [message, updateMessage] = useContext(MessageContext);

  return (
    <div>
      <p>message in message updater is.. {message}</p>
      <input type="text" value={message} onChange={(e) => updateMessage(e.target.value)} />
    </div>
  );
};

/**
 * Example of component that displays the message 
 * (all child components can use the message in the same way, without passing props)
 */
const App = () => 
{
  const [message] = useContext(MessageContext);

  useEffect(()=> {
    console.log('effect on message tirggered');
  }, [message]);

  return (
    <>
      <p>Message in app is.. {message}</p>
      <MessageUpdater />
    </>
  );
};

/**
 * Wrapps the App with the provider that holds the global message state and update function
 */
const AppContext = () =>
{
  return (
    <MessageProvider>
      <App />
    </MessageProvider>
  )
}

export default AppContext;

Upvotes: 1

xom9ikk
xom9ikk

Reputation: 2289

Your message inside App is not updated because you use useExternalHookAsState twice. The first time inside the MessageHandler, and the second time in App component. And they have different states. This way you only use setMessage in the child component of MessageHandler, which causes only that child component to be updated. And the App is not updated.

To solve this problem, raise the state and state management to the component in which you want to receive its updates. In your case, this is App. And then, through props, just pass the value and setter to the MessageHandler component.

The behavior you wanted to achieve is shown below:

import React, {useState, useEffect} from "react";

const useExternalHookAsState = () => {
  const [message, setMessage] = useState('nothing yet');

  const updateMessage = (newMessage) => {
    console.log('message should be update to..', newMessage);
    setMessage(newMessage);
  }

  return [message, updateMessage];
};

const MessageUpdater = ({message, onUpdate}) => {
  return (
    <div>
      <input type="text" value={message} onChange={(e) => onUpdate(e.target.value)}/>
    </div>
  );
};

const App = () => {

  const [message, updateMessage] = useExternalHookAsState();

  useEffect(() => {
    console.log('effect on message tirggered');
  }, [message]);

  return (
    <div>
      <p>message is.. {message}</p>
      <MessageUpdater message={message} onUpdate={updateMessage}/>
    </div>
  );
};

export default App;

Upvotes: 2

Related Questions