Search code examples
javascriptreactjscomponents

Passing data up through nested Components in React


Prefacing this with a thought; I think I might require a recursive component but that's beyond my current ability with native js and React so I feel like I have Swiss cheese understanding of React at this point.

The problem:

I have an array of metafields containing metafield objects with the following structure:

{
  metafields: [
    { 0: 
      { namespace: "namespaceVal", 
        key: "keyVal", 
        val: [
          0: "val1", 
          1: "val2", 
          2: "val3" 
        ]
      }
    }, 
    ...
  ]
}

My code maps metafields into Cards and within each card lives a component <MetafieldInput metafields={metafields['value']} /> and within that component the value array gets mapped to input fields. Overall it looks like:

// App
render() {
  const metafields = this.state.metafields;
  return (
    {metafields.map(metafield) => (
      <MetafieldInputs metafields={metafield['value']} />
    )}
  )
}

//MetafieldInputs
this.state = { metafields: this.props.metafields}

render() {
  const metafields = this.state;
  return (
    {metafields.map((meta, i) => (
      <TextField 
        value={meta}
        changeKey={meta}
        onChange={(val) => {
          this.setState(prevState => {
            return { metafields: prevState.metafields.map((field, j) => {
              if(j === i) { field = val; }
              return field;
            })};
          });
        }}
      />
    ))}
  )
}

Up to this point everything displays correctly and I can change the inputs! However the change happens one at a time, as in I hit a key then I have to click back into the input to add another character. It seems like everything gets re-rendered which is why I have to click back into the input to make another change.

Am I able to use components in this way? It feels like I'm working my way into nesting components but everything I've read says not to nest components. Am I overcomplicating this issue? The only solution I have is to rip out the React portion and take it to pure javascript.

guidance would be much appreciated!


Solution

  • My suggestion is that to out source the onChange handler, and the code can be understood a little bit more easier.

    Mainly React does not update state right after setState() is called, it does a batch job. Therefore it can happen that several setState calls are accessing one reference point. If you directly mutate the state, it can cause chaos as other state can use the updated state while doing the batch job.

    Also, if you out source onChange handler in the App level, you can change MetafieldInputs into a functional component rather than a class-bases component. Functional based component costs less than class based component and can boost the performance.

    Below are updated code, tested. I assume you use Material UI's TextField, but onChangeHandler should also work in your own component.

    // Full App.js
    import React, { Component } from 'react';
    import MetafieldInputs from './MetafieldInputs';
    
    class App extends Component {
        state = {
            metafields: [
                {
                    metafield:
                    {
                        namespace: "namespaceVal",
                        key: "keyVal",
                        val: [
                            { '0': "val1" },
                            { '1': "val2" },
                            { '2': "val3" }
                        ]
                    }
                },
            ]
        }
    
        // will never be triggered as from React point of view, the state never changes
        componentDidUpdate() {
            console.log('componentDidUpdate')
        }
    
        render() {
            const metafields = this.state.metafields;
            const metafieldsKeys = Object.keys(metafields);
            const renderInputs = metafieldsKeys.map(key => {
                const metafield = metafields[key];
                return <MetafieldInputs metafields={metafield.metafield.val} key={metafield.metafield.key} />;
            })
            return (
                <div>
                    {renderInputs}
                </div>
            )
        }
    }
    
    export default App;
    
    // full MetafieldInputs
    import React, { Component } from 'react'
    import TextField from '@material-ui/core/TextField';
    
    class MetafieldInputs extends Component {
        state = {
            metafields: this.props.metafields
        }
    
        onChangeHandler = (e, index) => {
            const value = e.target.value;
            this.setState(prevState => {
                const updateMetafields = [...prevState.metafields];
                const updatedFields = { ...updateMetafields[index] }
                updatedFields[index] = value
                updateMetafields[index] = updatedFields;
                return { metafields: updateMetafields }
            })
        }
    
        render() {
            const { metafields } = this.state;
            // will always remain the same
            console.log('this.props', this.props)
            return (
                <div>
                    {metafields.map((meta, i) => {
                        return (
                            <TextField
                                value={meta[i]}
                                changekey={meta}
                                onChange={(e) => this.onChangeHandler(e, i)}
                                // generally it is not a good idea to use index as a key.
                                key={i}
                            />
                        )
                    }
                    )}
                </div>
            )
        }
    }
    
    export default MetafieldInputs
    

    Again, IF you out source the onChangeHandler to App class, MetafieldInputs can be a pure functional component, and all the state management can be done in the App class.

    On the other hand, if you want to keep a pure and clean App class, you can also store metafields into MetafieldInputs class in case you might need some other logic in your application.

    For instance, your application renders more components than the example does, and MetafieldInputs should not be rendered until something happened. If you fetch data from server end, it is better to fetch the data when it is needed rather than fetching all the data in the App component.