0

I've got a user that has some data on the page in this form:

[{ name: 'Jack', id: 1 }, { name: 'Ellen', id: 2 }, {name: 'Nick', id: 3}]

An Apollo query loads this data. The user can add names to or delete names from the page using a form. An Apollo mutation handles the add or delete requests. RefetchQueries gets called after the mutation, the state gets updated and the list of names gets rerendered.

The problem is that if I add a name, refetchQueries will return all 4 objects and, in return, all 4 objects get set to state.

[{ name: 'Jack', id: 1 }, { name: 'Ellen', id: 2 }, {name: 'Nick', id: 3}, { name: 'Mary', id: 4 }]

I would like to find out how to get the difference between the new and the old data. Then add or delete the right object from the state array, and not entirely reset the state. The id's used will always be unique.

loadData = (names, that) => {

        that.setState({ loading:true });

        let data = {};
        data.names= [];

        const oldNames = that.state.names;

        names.map(name=> (
            data.names.push({ name: name.name, id: name.id })
        ));

       that.setState({names: data.names, loading: false });
}

Above function gets called in componentWillUpdate like this:

componentWillUpdate(nextProps, nextState) {
    loadData(nextProps.names, this);
}

I am trying to change this function as follow:

updateData = (names, that) => {

        let data = {};
        data.names = [];

        const oldNames = that.state.names ;

        names.map(name => (
            data.names.push({ name: name.name, id: name.id })
        ));

        let difference = _(data.names)
            .differenceBy(oldNames, 'id', 'name')
            .map(_.partial(_.pick, _, 'id', 'name'))
            .value();

        if (data.names.length > oldNames.length)
        that.setState(prevState => ({
            names: [...prevState.names, {name: difference[0].name, id: difference[0].id}]
        }))
}

But to find the difference between the old and new data in this way doesn't feel very good. I've tried solutions from Compare 2 arrays which returns difference, but I couldn't get it to work. I hope you can help me introduce a better way to do find the difference. In the example above I used lodash, but that's not a requirement. I also wonder if there is a better way. For example, directly get the result from the Apollo mutation and add this result to the State.

UPDATE mapping of nodes:

render() {

        if (this.state.loading) { return <div>Loading...</div> }

        const links = this.state.links.map( (link,i) => {
            return (
                <Link
                    linkClass={this.props.linkClass}
                    key={link.target+i}
                    data={link}
                />);
        });

        const nodes = this.state.nodes.map( (node) => {
            return (
                <Node
                    nodeClass={this.props.nodeClass}
                    data={node}
                    label={node.label}
                    key={node.id}
                    onClick={this.onClickHandler.bind(this, node)}
                />);
        });

        return (
            <div className="graphContainer">
                <svg className="graph" width={FORCE.width} height={FORCE.height}>
                    <g >
                        {links}
                    </g>
                    <g >
                        {nodes}
                    </g>
                </svg>
            </div>
        );
    }
}
Vialito
  • 523
  • 7
  • 28
  • What are you hoping to achieve? Based on the code you have written, there won't be any performance gains as all components using `this.state.names` will get re-rendered. If you want to reduce the payload size from the API calls, then I have some ideas. – Roy Wang Apr 27 '18 at 05:42
  • The names get displayed using d3. So if I reload the entire state, the entire d3 graph gets rerendered. I want to only add or delete single elements to the d3 graph. I also wanted to find a way to get the difference using Javascript at first, but I am not sure how to find it. I would like to get this part to work, but any performance gains would be really appreaciated too – Vialito Apr 27 '18 at 05:51
  • Add the code where you use `this.state.names` and the graph component. – Roy Wang Apr 27 '18 at 05:55
  • `this.state.names` gets used when loading / updating the data and further only to initialize the force. I'm not sure how to go about showing all the graph code, it's more than 300 lines in a handful of different components. What is it you are you looking for? – Vialito Apr 27 '18 at 06:29
  • Thing is, I'm not exactly sure what is going on in the lodash code above, I really want to return the difference between the old and new array of objects. The code above works when I add a name. But if I delete a name, the deleted object doesn't get returned. Later a user might add or delete multiple names or edit objects. This lodash code can't help with all of that.. So I wonder how one could get the objects that were added, deleted or edited. I feel like the d3 graph is not really a part of that problem, excuse me if I'm wrong or didn't ask the question clearly enough. – Vialito Apr 27 '18 at 06:29
  • What you are trying to do is not going to help. Does your graph component takes the entire array as prop or are you doing a map to an array of node components? The component need to be pure (if state is shallow) or have `componentShouldUpdate` to avoid re-rendering when the node did not change. – Roy Wang Apr 27 '18 at 06:38
  • Yes, the loadData function above takes the entire prop from Apollo and then sets it to state. The graph component maps over this state array and creates a svg circle for every object in the array. Should I post this part of the code? When I add a node using this code, it seems as if a new circle comes flying in really nice without redrawing the entire graph. But I might be wrong.. – Vialito Apr 27 '18 at 06:47
  • If the mapped component has an unique key then React will do the optimisation to not re-render unnecessarily. See https://reactjs.org/docs/lists-and-keys.html#keys. You can post the part where the mapping is done. – Roy Wang Apr 27 '18 at 06:51
  • I updated the question with the code of the mapping in the graph component. React doesn't throw a missing key error – Vialito Apr 27 '18 at 06:56
  • You have provided the `key` prop. `` seems fine but for `` you shouldn't use the index as part of the key (refer to the reactjs link in above comment). – Roy Wang Apr 27 '18 at 07:01
  • Thanks, I updated the link's index with the id's of the lines. But is my assumption that you can update a d3 graph without entirely redrawing it this way wrong? So it doesn't help to create a function to find the objects that were added or deleted and add or delete them to the state? – Vialito Apr 27 '18 at 07:09
  • explanation is getting lengthy; added it as answer – Roy Wang Apr 27 '18 at 07:23

1 Answers1

1

When you call this.setState({ names: newArray }), newArray must be a newly constructed array (since you cannot mutate state directly), so it doesn't make a difference whether you construct this array based on the difference or just use the result from an API call.

In other words, there's no difference between setState({ names: [1, 2, 3] }) or setState({ names: [...someOldState, 3] }) as you are still setting names to a newly built array in both cases, so your attempt at calculating the difference will not help your intention in any way.

React does the optimisation of only re-rendering the components with changed keys so you just have to make sure an unique and stable key is provided in the mapped component.

This optimisation is done at the virtual DOM level though. If you want to avoid render being called in unchanged Link and Node components, they need to be declared as PureComponent, or with a shouldComponentUpdate function if the props are not shallow.

Roy Wang
  • 11,112
  • 2
  • 21
  • 42
  • Thank you so much for all your effort. I didn't realize setState works like this. Something strange is happening though. When `loadData(data)` gets called with a an added name, the entire graph, including the new name, pop up in the middle of the container. If I call `updateData(data)` The graph will tick, but the new node comes floating in nicely from the top-left corner of the container. The only difference between both functions is the way the state is set right? Why would a different behaviour occur? – Vialito Apr 27 '18 at 08:21
  • If the resulting array in `this.setState({ names: array })` is identical (try log it) it shouldn't make a difference, unless u did some direct state mutation somewhere. Does removing the `loading` state make it the same? – Roy Wang Apr 27 '18 at 08:31
  • I removed `loading` and it's still the same. That loading part apparantly isn't helping anyway anymore. I found a way to get the deleted object. Then set the state in a seemingly ridiculous way with a for loop and splice the node to be deleted. Strangly enough, I do get the desired result. `loadData` and `updateData` are the only places in which I call setState. The only other thing I can think of is that I use d3's `.datum` in the link and node component. It seems to work, it's just that I'm not sure why plus the fact that it all seems a bit sketchy. – Vialito Apr 27 '18 at 17:45