2

I am building a React web app which is split up in multiple components accessible via react-tabs:

import React from 'react';
import ReactDOM from 'react-dom';
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import a from './components/a';
import b from './components/b';

const TabNavigator = () => (
    <Tabs>
        <TabList>
            <Tab> A </Tab>
            <Tab> B </Tab>
        </TabList>

        <TabPanel>
            <a />
        </TabPanel>
        <TabPanel>
            <b />
        </TabPanel>
    </Tabs>
);

ReactDOM.render(<TabNavigator />, document.getElementById('root'));

Every tab is his own component/sub-system which is rendered freshly when the tab is accessed. In every tab I am using the data of one JSON-file. This data is loaded into the state of each component like this:

constructor(props) {
    super(props);

    this.state = {
        data: json
    };
}

I am now changing the state in one of the components to trigger a re-render with the new data:

this.setState({
    data: editedJson
});

So far so good but when I am now switching to the other tab/component this.state.data has changed there as well - why did this happen? Is the state shared between the components?

EDIT: Here is an MVCE where you can change the state in B and it will also change in A:

Edit tabs-parent-pass-state

leonheess
  • 16,068
  • 14
  • 77
  • 112
  • 4
    It may be more helpoful to provide a [minimal, reproducible, and complete](https://stackoverflow.com/help/minimal-reproducible-example) code sample that reproduces your issue. At a minimum you should at lease share complete code. For example, here it would be better to include your tab components code to see what/where/how it is setting state. – Drew Reese Jul 29 '19 at 14:37
  • 1
    When your `Tabs` gets loaded every `TabPanel` get loaded by default, since you are having same state name in both the component, your component are able to access each others state. Change state name in one of the component. – ravibagul91 Jul 29 '19 at 14:40
  • 1
    Other way is conditionally render `TabPanel`, hav a onClick on `Tabs` maintain a state and using that state render `TabPanel`. – ravibagul91 Jul 29 '19 at 14:43
  • Another possibility is that you are using the same pointer to the fetched data. For each tab panel component, you should make a copy of your data like so: `this.state = { data: { ...json } };` – Chris Jul 29 '19 at 14:50
  • Let me start by saying that state information is localized to the component itself. Other components cannot know of the state without explicitly passing that data into other components. As the other have said, please post a fully reproducible example. Possibly include component A and B – alaboudi Jul 29 '19 at 14:56
  • If you use a reference to the same object in every component then they're all modifying the same object; simple as that. – Dave Newton Jul 29 '19 at 15:43
  • @DrewReese I edited my post accordingly – leonheess Jul 29 '19 at 16:32
  • https://codesandbox.io/s/peaceful-northcutt-3e2tk – Dave Newton Jul 29 '19 at 19:26

3 Answers3

1

Seems when you import that JSON file you are creating an in-memory version of it that both components are then referencing. If you copy the values (spread) as @Christiaan suggests in the constructor then you avoid mutating the original, which is bad practice.

Edit distracted-poitras-jg1eb

A better suggestion may be to import your JSON once in the common ancestor and pass references, or copied instances, to each tab... really depends on what each tab really needs to do with that data.

You should note that each tab is getting mounted/unmouted whenever you switch tabs, so the constructor logic is run every time. If you want state changes to persist then you must loft it to a parent component, i.e. your Tabs component in index.js, and pass to each child tab what data they need.

Edit tabs-parent-pass-state

There were actually a couple factors working against you. The first is the fact that imports are cached (explanation here), so each time a tab is mounted the json import returns the cached reference which is stored in data and in your constructor you save that reference value to state.data. The second is your setState function wasn't quite a pure function. It merely copies the reference again and mutates the array it points to when really you want to create a new array and shallow copy all the elements before adding a new element. Below is another sandbox that should help illustrate this.

Edit mutate-imported-json

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Could you maybe alborate on that line: `const [dataA, setDataA] = useState(data);`? – leonheess Jul 30 '19 at 09:24
  • Read [React's `useState` documentation](https://reactjs.org/docs/hooks-reference.html#usestate) for more information about how to use it, and how to use React's Hooks in general. – guzmonne Jul 30 '19 at 13:18
  • Second @guzmonne. `useState` is a react hook for adding state to a functional component, it takes an initial value and returns an array of two elements, index 0 is the state value and index 1 is the state mutator function. If the syntax looks a little funny it is because the pattern also uses array destructuring to name the return values. – Drew Reese Jul 30 '19 at 13:45
  • @DrewReese Thanks. I am also still confused about how this accidental global state happened in the first place. If you find the time to edit your answer toe elaborate on that I would be very grateful as well – leonheess Jul 30 '19 at 14:36
  • 1
    @MiXT4PE Updated answer with some elaboration. – Drew Reese Jul 31 '19 at 07:27
1

I was able to replicate your issue like this:

import React from "react";
import ReactDOM from "react-dom";
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";

import "./styles.css";

let json = { title: "I am a title" };

function Content({tab}) {
  const [state, setState] = React.useState(json);

  return (
    <div>
      <h1>{tab}</h1>
      <h2>{state.title}</h2>
      <label>New Title</label>
      <input type="text" value={state.title} onChange={handleChange} />
    </div>
  );

  function handleChange(e) {
    json = { title: e.target.value };
    setState(json);
  }
}

function TabNavigator() {
  return (
    <Tabs>
      <TabList>
        <Tab>A</Tab>
        <Tab>B</Tab>
      </TabList>
      <TabPanel><Content name="A" /></TabPanel>
      <TabPanel><Content name="B"/></TabPanel>
    </Tabs>
  );
}

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

The problem comes when you update the "global" object json, and then use it to update the component state. See the handleChange function; I am updating the json object first, and then setting the new component state with it.

When you shift to the other tab, a new component is created, this component will have its state instantiated from the "global" json object, and so, will have the same content as the other tab. This process repeats as you toggle between them.

If you remove this assignment to update the state then the problem is solved (just do setState({title: e.target.value}). But you can't persist changes. To solve this, I suggest using React Context. Here is a link to a CodeSandbox where you can see it in action.

I hope it helps.

guzmonne
  • 2,490
  • 1
  • 16
  • 22
0

Your json in all components has same pointer, so all components access to same object and change that.

To fix this you must create whole new object for every component, like this:

this.state = {
    data: { ...json }
};
farbod
  • 91
  • 7