1
votes

I have issues with communication between a parent and a child component.

I would like the parent (Host) to hold his own state. I would like the child (Guest) to be passed that state and modify it. The child has his local version of the state which can change however the child wants. However, once the child finishes playing with the state, he passes it up to the parent to actually "Save" the actual state. How would I correctly implement this?

Issues from my code:

  • on the updateGlobalData handler, I log both data and newDataFromGuest and they are the same. I would like data to represent the old version of the data, and newDataFromGuest to represent the new

  • updateGlobalData is being called 2X. I can solve this by removing the updateGlobalData ref from the deps array inside useEffect but I don't want to heck it.

My desired results should be:

  • the data state should hold the old data until updateGlobalData is called
  • I want updateGlobalData to be fired only once when I click the button

Code from Codesandbox:

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

const Host = () => {
  const [data, setData] = useState({ foo: { bar: 1 } });

  const updateGlobalData = newDataFromGuest => {
    console.log(data);
    console.log(newDataFromGuest);

    setData(newDataFromGuest);
  };

  return <Guest data={data} updateGlobalData={updateGlobalData} />;
};

const Guest = ({ data, updateGlobalData }) => {
  const [localData, setLocalData] = useState(data);
  const changeLocalData = newBarNumber => {
    localData.foo = { bar: newBarNumber };
    setLocalData({ ...localData });
  };

  useEffect(() => {
    updateGlobalData(localData);
  }, [localData, updateGlobalData]);

  return (
    <div>
      <span>{localData.foo.bar}</span> <br />
      <button onClick={() => changeLocalData(++localData.foo.bar)}>
        Increment
      </button>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<Host />, rootElement);
1
RonH despite the fact your question was put on hold, I may have the answer for you. Please, edit the question as requested above and I will provide the solution. - Max Tomasello
I did. Hopefully it is off hold soon and you can share the solution with me - RonH
@ravibagul91 could you please take the "on hold" status for this question away? The answer has been edited. Thanks in advance! - Max Tomasello

1 Answers

2
votes

NOTE: Code solution below

  • Problem 1:

I want updateGlobalData to be fired only once when I click the button

To solve this issue, I have used a mix between React.createContext and the hook useReducer. The idea is to make the Host dispatcher available through its context. This way, you do not need to send the "updateGlobalData" callback down to the Guest, nor make the useEffect hook to be dependant of it. Thus, useEffect will be triggered only once.

Note though, that useEffect now depends on the host dipatcher and you need to include it on its dependencies. Nevertheless, if you read the first note on useReducer, a dispatcher is stable and will not cause a re-render.

  • Problem 2:

the data state should hold the old data until updateGlobalData is called

The solution is easy: DO NOT CHANGE STATE DATA DIRECTLY!! Remember that most values in Javascript are passed by reference. If you send data to the Guest and you directly modify it, like here

const changeLocalData = newBarNumber => {
  localData.foo = { bar: newBarNumber }; // YOU ARE MODIFYING STATE DIRECTLY!!!
  ...
};

and here

<button onClick={() => changeLocalData(++localData.foo.bar)}> // ++ OPERATOR MODIFYES STATE DIRECLTY

they will also be modified in the Host, unless you change that data through the useState hook. I think (not 100% sure) this is because localData in Guest is initialized with the same reference as data coming from Host. So, if you change it DIRECTLY in Guest, it will also be changed in Host. Just add 1 to the value of your local data in order to update the Guest state, without using the ++ operator. Like this:

localData.foo.bar + 1

This is my solution:

import React, { useState, useEffect, useReducer, useContext } from "react";
import ReactDOM from "react-dom";

const HostContext = React.createContext(null);

function hostReducer(state, action) {
  switch (action.type) {
    case "setState":
      console.log("previous Host data value", state);
      console.log("new Host data value", action.payload);
      return action.payload;
    default:
      throw new Error();
  }
}

const Host = () => {
  // const [data, setData] = useState({ foo: { bar: 1 } });
  // Note: `dispatch` won't change between re-renders
  const [data, dispatch] = useReducer(hostReducer, { foo: { bar: 1 } });

  // const updateGlobalData = newDataFromGuest => {
  //   console.log(data.foo.bar);
  //   console.log(newDataFromGuest.foo.bar);

  //   setData(newDataFromGuest);
  // };

  return (
    <HostContext.Provider value={dispatch}>
      <Guest data={data} /*updateGlobalData={updateGlobalData}*/ />
    </HostContext.Provider>
  );
};

const Guest = ({ data /*, updateGlobalData*/ }) => {
  // If we want to perform an action, we can get dispatch from context.
  const hostDispatch = useContext(HostContext);
  const [localData, setLocalData] = useState(data);
  const changeLocalData = newBarNumber => {
    // localData.foo = { bar: newBarNumber };
    // setLocalData({ ...localData });
    setLocalData({ foo: { bar: newBarNumber } });
  };

  useEffect(() => {
    console.log("useEffect", localData);
    hostDispatch({ type: "setState", payload: localData });
    // updateGlobalData(localData);
  }, [localData, hostDispatch /*, updateGlobalData*/]);

  return (
    <div>
      <span>{localData.foo.bar}</span> <br />
      <button onClick={() => changeLocalData(localData.foo.bar + 1)}>
        Increment
      </button>
    </div>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<Host />, rootElement);

If you see anything not matching with what you want, please, let me know and I will re-check it.

I hope it helps.

Best, Max.