userlkjsflkdsvm
userlkjsflkdsvm

Reputation: 973

React Context initial state is being mutated - JavaScript / React

I am making a guide me section. What this guide me section does - displays an array of processes. Within each process is an array of steps, within each step is an array of options. The user selects an option from one of the steps, it takes them to the next correlating step. If the user selects the option on step 2, it could take them to step 3 or back to step 1. It depends on the id.

With all that said I'm having issues with my Process mutating on me. I'm using React Context as a global state. When a user selects an option, I'm grabbing that id, then filtering the designated object by that id. So I should only be left is that processes with that specific step. What's happening is my initial global state is mutating somehow. I'm missing something here as I'm new to React.

P.s - I'm not using any services at this moment, so I just copied some JSON over to my initial state in context.js

context.js

import React, { Component } from 'react'
// import axios from 'axios'

const Context = React.createContext()
const reducer = (state, action) => {
    switch(action.type){
        case 'SEARCH_PROCESS':
        return {
            ...state,
            guides: action.payload
        }
        default:
        return state
    }
}

export class Provider extends Component {

    state = {
        guides: [
            {
                "processName": "Support Messaging",
                "steps": [{
                    "id": "15869594739302",
                    "title": "step one", 
                    "options": [{
                        "nextStep": "4767fn-47587n-2819am-9585j,04956840987", 
                        "text": "Option 1 text", 
                        "type": "option"
                      },
                      {
                        "nextStep": "4767fn-47587n-2819am-9585j,04956840987", 
                        "text": "Option 2 text",
                        "type": "option"
                      },
                      {
                        "nextStep": "", 
                        "text": "Option 3 text",
                        "type": "option"
                      }
                    ]
                  },
                  {
                    "id": "04956840987",
                    "title": "step two", 
                    "options": [{
                        "nextStep": "4767fn-47587n-2819am-9585j,15869594739302",
                        "text": "Return to step1", 
                        "type": "option"
                      },
                      {
                        "nextStep": "", 
                        "text": "Option 2 text",
                        "type": "option"
                      },
                      {
                        "nextStep": "",
                        "text": "Option 3 text",
                        "type": "option"
                      }
                    ]
                  }
                ],
                "owner": "bob",
                "id": "4767fn-47587n-2819am-9585j",
                "lastUpdated": 154222227099000, 
                "tags": ["Tag1", "Tag2", "Tag3"]
              }
                ],
                "owner": "bob",
                "id": "4767fn-47587n-2819am-9585x",
                "lastUpdated": 154222227099000, 
                "tags": ["Tag1", "Tag2", "Tag3"]
              }
        ],
        initialGuide: [
            {
                "processName": "Support Messaging",
                "steps": [{
                    "id": "15869594739302",
                    "title": "step one", 
                    "options": [{
                        "nextStep": "4767fn-47587n-2819am-9585j,04956840987", 
                        "text": "Option 1 text", 
                        "type": "option"
                      },
                      {
                        "nextStep": "4767fn-47587n-2819am-9585j,04956840987", 
                        "text": "Option 2 text",
                        "type": "option"
                      },
                      {
                        "nextStep": "", 
                        "text": "Option 3 text",
                        "type": "option"
                      }
                    ]
                  },
                  {
                    "id": "04956840987",
                    "title": "step two", 
                    "options": [{
                        "nextStep": "4767fn-47587n-2819am-9585j,15869594739302",
                        "text": "Return to step1", 
                        "type": "option"
                      },
                      {
                        "nextStep": "", 
                        "text": "Option 2 text",
                        "type": "option"
                      },
                      {
                        "nextStep": "",
                        "text": "Option 3 text",
                        "type": "option"
                      }
                    ]
                  }
                ],
                "owner": "bob",
                "id": "4767fn-47587n-2819am-9585j",
                "lastUpdated": 154222227099000, 
                "tags": ["Tag1", "Tag2", "Tag3"]
              }
        ],

        dispatch: action => this.setState(state => reducer(state, action))
    }



    render() {
        return (
            <Context.Provider value={this.state}>
                {this.props.children}
            </Context.Provider>
        )
  }
}

export const Consumer = Context.Consumer;

Guides.js

import React, { Component } from 'react'
import { Consumer } from '../../context'
import Process from './Process'

class Guides extends Component {
  constructor (props) {
    super(props)
    this.state = {
      contextValue: [],
      searchData: props.location.data
    }
  }

  render () {
      console.log(this.props.location.data, this.state, 'logging state and props on guides')
    //   this.state.searchData = this.props.location.data

    return (
      <Consumer>
        {value => {

          return (
            <React.Fragment>
              <div className="content-wrapper">
                <h1>Guide Me</h1>
                <div className="ms-Grid times--max-width" dir="ltr">
                  <div className="ms-Grid-row">
                    <div className="profile--wrapper ms-Grid-col ms-sm12 ms-md12 ms-lg5">
                      {value.guides.map(item => {
                        return <Guide key={item.id} guide={item} processValue={value.guides} initialGuide={value.initialGuide}/>
                      })}
                    </div>
                  </div>
                </div>
              </div>
            </React.Fragment>
          )
        }}
      </Consumer>
    )
  }
}

export default Guides

Process.js

import React, { Component } from 'react'
import GuideSteps from './Guide-Steps'
import { Consumer } from '../../context'

class Process extends Component {
  constructor(props) {
    super(props)
    this.state = {
      processName: this.props.guide.processName,
      process: this.props.guide,
      steps: this.props.guide.steps,
      selectedIndex: 0,
      selectedStep: '',
      processValue: this.props.processValue,
      initialGuide: this.props.initialGuide
    }

    this.displayStep = this.displayStep.bind(this)
  }

  displayStep = (res, dispatch) => {
    this.setState({ selectedStep: res })
  }

  render() {

    const { steps, selectedIndex, process, processName, processValue, initialGuide } = this.state


    return (
        <Consumer>
          {value => {
          return (
            <div>
              <h2 className="profile--sub-header--bold">{processName}</h2>

              <GuideSteps
                key={this.props.guide.steps[selectedIndex].id}
                selectedStep={this.props.guide.steps[selectedIndex]}
                stepValue={this.displayStep}
                process={process}
                processValue={processValue}
                initialGuide={initialGuide}
              />
            </div>
          )
          }}
          </Consumer>
          )

  }
}

export default Process

Guide-Steps.js

import React, { Component } from 'react'
import { ChoiceGroup } from 'office-ui-fabric-react/lib/ChoiceGroup'
import { Consumer } from '../../context'

class GuideSteps extends Component {

    constructor(props) {
        super(props);
        this.state = {
            process: [],
            selectedStep: this.props.selectedStep,
            dispatch: '',
            processValue: this.props.processValue,
            initialGuide: ''
        }

        this._onChange = this._onChange.bind(this)
    }

  _onChange = (ev, option) => {
    // this.props.stepValue(option.value.nextStep)
    const { dispatch , initialGuide } = this.state

    let optionArray = option.value.nextStep.split(',')

    let processArray = this.state.process.filter(item => {
        return item.id === optionArray[0]
    })

    let stepArray = processArray[0].steps.filter(item => {
        return item.id === optionArray[1]
    })

    console.log(stepArray, processArray, this.state.process, 'logging step array before setting')

    processArray[0].steps = stepArray

    console.log(stepArray, processArray, this.state.process, 'logging step array after setting')


    dispatch({
        type: 'SEARCH_PROCESS',
        payload: processArray
      })
  }

  render() {
    let options = []
    {
      this.props.selectedStep.options.map(item => {
        return options.push({
          key: item.text,
          text: item.text,
          value: item
        })
      })
    }
    return (
        <Consumer>
            {value => {
                const { dispatch, guides, initialGuide } = value
                this.state.dispatch = dispatch
                console.log(value, 'logging initial guide in render')
                this.state.process = initialGuide
    return (
      <div>
        <ChoiceGroup
          className="defaultChoiceGroup"
          options={options}
          onChange={this._onChange}
        />
      </div>
    )
    }}
    </Consumer>
    )
  }
}

export default GuideSteps

On change in GuideSteps is where I'm doing the logic for filtering and setting up my new object.

EDIT

This fixed the issue but I think it's too expensive. How would I go about solving this issue without having to reparse the array.

update: (ev, option) => {
      const { initialGuide } = this.state

      if (option.value.nextStep !== null && option.value.nextStep !== '') {
        //split string
        const optionArray = option.value.nextStep.split(',')

        //filter process array
        const processArray = initialGuide.filter(process => {
          return process.id === optionArray[0]
        })
        //filter step array
        const stepArray = processArray[0].steps.filter(
          item => item.id === optionArray[1]
        )

        if(stepArray.length > 0 && stepArray !== null) {
            //get a copy of the process array so original is not mutated by the steps
            let stringC = JSON.stringify(processArray)
            let stringD = JSON.parse(stringC)
            stringD[0].steps = stepArray

            //issue might be here visually where setting the state happens quickly, therefore radio button visual does not display in time.
            setTimeout(() => {
                this.setState({ guides: stringD })
              }, 200)
        }
      }
    }, 

Upvotes: 3

Views: 956

Answers (1)

lecstor
lecstor

Reputation: 5707

this.state.process = initialGuide

let processArray = this.state.process.filter...

processArray[0].steps = stepArray

So it looks like you're mutating initialGuide via reference.

Upvotes: 6

Related Questions