React Context initial state is being mutated - JavaScript / React - javascript

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)
}
}
},

this.state.process = initialGuide
let processArray = this.state.process.filter...
processArray[0].steps = stepArray
So it looks like you're mutating initialGuide via reference.

Related

React class component, data being initialized as undefined and then populated. unable to map

I am developing an application using react class components. Below is my component. Data is fetched asynchronously from an API endpoint.
import React, { Component } from 'react';
...etc etc
import { doGetCompilation } from '../controllers/compilationController';
class Forms extends Component {
constructor() {
super();
this.state = {
step: 1,
};
this.getCompilation = this.getCompilation.bind(this);
}
state = {
step: 1,
compilation: []
};
componentDidMount() {
this.setState({ isLoading: true });
this.getCompilation();
}
async getCompilation() {
this.setState({ isLoading: true });
try {
let data = await doGetCompilation('ceb9baad7baefeb4d9d94cae8082452f53563baf');
this.setState({ isLoading: false, compilation: data.data });
} catch (error) {
console.log(error);
}
}
render() {
const {
step,
compilation,
isLoading,
} = this.state;
console.log('compilation', compilation);
return (
<>
{isLoading ? (
<div>Loading...</div>
) : (compilation && Array.isArray(compilation.steps) &&
compilation.steps.map((step, index) => (
<Paper variant="outlined" sx={{ my: { xs: 3, md: 6 }, p: { xs: 2, md: 3 } }}>
<Stepper
steps={step}
activeStep={0}
/>
<Form/>
</Paper >
)))
}
</>
)
}
}
export default Forms;
I'm getting an error while mapping over my compilation object. This object is undefined initially and then it is populated on componentDidMount. But this is not something react seem to like. In fact no matter what i can not render my component. Can you suggest a solution or an interesting article to read about this behaviour.
The error i get is :
Forms.jsx:81 Uncaught TypeError: Cannot read properties of undefined (reading 'map')
object:
[
{
"id": 3,
"token": "ceb9baad7baefeb4d9d94cae8082452f53563baf",
"form": {
"id": 17,
"name": "aziende"
},
"steps": [
{
"id": 4,
"name": "STEP 2 - Informazioni lavorative",
"weight": null,
"questions": []
},
{
"id": 5,
"name": "STEP 2 - Informazioni personali",
"weight": null,
"questions": []
},
{
"id": 3,
"name": "STEP 1 - Informazioni preliminari",
"weight": null,
"questions": []
}
]
}
]
try to put the compilation: [] inside the state in the constructor

Pass dynamic content to the react table sub component

I am using react table v6 for grid purposes. I am trying to implement a subcomponent where in the data to this sub component needs to be passed dynamically. This sub component should expand and collapse on click of arrows. I have tried the following , but the sub component is not rendering any data. I am creating a wrapper for this, the data to the subcomponent should be passed dynamically based on the source data.
https://codesandbox.io/s/react-table-row-table-subcompoentn-sk14i?file=/src/DataGrid.js
import * as React from "react";
import ReactTable from "react-table";
import "react-table/react-table.css";
export default class DataGrid extends React.Component {
renderSubComponent = original => {
console.log(original);
return (
original.nested &&
original.nested.map((i, key) => (
<React.Fragment key={key}>
<div>{i.name}</div>
<div>{i.value}</div>
</React.Fragment>
))
);
};
render() {
return (
<ReactTable
data={this.props.data}
columns={this.props.columns}
SubComponent={this.renderSubComponent}
/>
);
}
}
import * as React from "react";
import { render } from "react-dom";
import DataGrid from "./DataGrid";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
columns: [],
allData: []
};
}
componentDidMount() {
this.getData();
this.getColumns();
}
getData = () => {
const data = [
{
firstName: "Jack",
status: "Submitted",
nested: [
{
name: "test1",
value: "NA"
},
{
name: "test2",
value: "NA"
}
],
age: "14"
},
{
firstName: "Simon",
status: "Pending",
nested: [
{
name: "test3",
value: "NA"
},
{
name: "test4",
value: "Go"
}
],
age: "15"
}
];
this.setState({ data });
};
getColumns = () => {
const columns = [
{
Header: "First Name",
accessor: "firstName"
},
{
Header: "Status",
accessor: "status"
},
{
Header: "Age",
accessor: "age"
}
];
this.setState({ columns });
};
onClickRow = rowInfo => {
this.setState({ allData: rowInfo }, () => {
console.log(this.state.allData);
});
};
render() {
return (
<>
<DataGrid
data={this.state.data}
columns={this.state.columns}
rowClicked={this.onClickRow}
/>
</>
);
}
}
render(<App />, document.getElementById("root"));
try spread the argument and it will work :
...
renderSubComponent = ({original}) => {
...

How to automatically move JSON data depending on Boolean value [duplicate]

This question already has answers here:
How to sort array of objects based on a boolean property?
(2 answers)
Javascript sort array of objects by a boolean property
(14 answers)
Closed 3 years ago.
I have a JSON file:
[
{
"id": 1,
"color": "Blue",
"availability": false
},
{
"id": 2,
"color": "Pink",
"availability": true
}
]
What I would like to achieve is for the JSON with "availability : true" to automatically appear above the "availability : false". So that the pink appears above the blue like this:
This is my code so far which simply displays them in the same order as the JSON file:
import React, { Component } from 'react';
import './styles.css'
class GetOnlinePosts extends Component {
constructor(props){
super(props);
this.state = {
error : null,
isLoaded : false,
posts : []
};
}
componentDidMount(){
fetch("https://api.myjson.com")
.then( response => response.json())
.then(
(result) => {
this.setState({
isLoaded : true,
posts : result
});
},
(error) => {
this.setState({
isLoaded: true,
error
})
},
)
}
render() {
const {error, isLoaded, posts} = this.state;
if(error){
return <div>Error in loading</div>
}else if (!isLoaded) {
return <div>Loading ...</div>
}else{
return(
<div>
<div className="tiles">
{
posts.map(post => (
<div key={post.id}>
<div className="tile">
<p className="color">{post.color}</p>
</div>
</div>
))
}
</div>
</div>
);
}
}
}
export default GetOnlinePosts;
I am unsure of how to achieve this and have struggled to find any suggestions so far so any help would be great.
var data = [{
"id": 1,
"color": "Blue",
"availability": false
},
{
"id": 2,
"color": "Pink",
"availability": true
},
{
"id": 3,
"color": "Pink",
"availability": true
},
{
"id": 4,
"color": "Pink",
"availability": false
}, {
"id": 5,
"color": "Pink",
"availability": true
}
]
//unordered
data.sort(val => val.availability ? -1 : 1) //simple one line sort function for boolean
console.log(data);
// ordered based on your default array order
data = [{
"id": 1,
"color": "Blue",
"availability": false
},
{
"id": 2,
"color": "Pink",
"availability": true
},
{
"id": 3,
"color": "Pink",
"availability": true
},
{
"id": 4,
"color": "Pink",
"availability": false
}, {
"id": 5,
"color": "Pink",
"availability": true
}
]
data.sort((a, b) => b.availability - a.availability);
console.log(data);
You can filter the two sets, create a new array and then render it.
import React, { Component } from 'react';
import './styles.css'
class GetOnlinePosts extends Component {
constructor(props){
super(props);
this.state = {
error : null,
isLoaded : false,
posts : []
};
}
componentDidMount(){
fetch("https://api.myjson.com")
.then( response => response.json())
.then(
(result) => {
this.setState({
isLoaded : true,
posts : result
});
},
(error) => {
this.setState({
isLoaded: true,
error
})
},
)
}
render() {
const {error, isLoaded, posts} = this.state;
const orderedPosts = [...posts.filter((post) => post.availability), ...posts.filter((post) => !post.availability)]
if(error){
return <div>Error in loading</div>
}else if (!isLoaded) {
return <div>Loading ...</div>
}else{
return(
<div>
<div className="tiles">
{
orderedPosts.map(post => (
<div key={post.id}>
<div className="tile">
<p className="color">{post.color}</p>
</div>
</div>
))
}
</div>
</div>
);
}
}
}
export default GetOnlinePosts;

How to get objects in array of object in React Native

I'm kind of lost to access some info in my static data. Here's the data :
{
"info1": {
"label": "label",
"class": "class-css",
"title": "title",
"text": "text",
"number": "20",
"tags": [
{
"name": "#twitter"
},
{
"name": "#myspace"
}
]
},
"info2": {
"label": "label",
"class": "class-css",
"title": "title",
"text": "text",
"number": "20",
"tags": [
{
"name": "#instagram"
},
{
"name": "#facebook"
}
]
}
}
Then I get the first info like that :
this.setState({
currentLabel: this.state.labels["info1"]
})
This is why I want and then I want to display info in a component and it's working until I try to get tags information. I tried a .map() but without success and error.
<View>
<Text>{infoDetail.title}</Text>
<Text>{infoDetail.text}</Text>
<Text>How do I get "tags" information</Text>
</View>
Is it possible to access these objects in the array "tags" ?
yes you can call tags as follows infoDetail.tags and do map on it
render(){
const tagItems = infoDetail && infoDetail.tags.map((item, index) => {
return <Text key={index}>{item.name}</Text>
});
return(
<View>
<Text>{infoDetail.title}</Text>
<Text>{infoDetail.text}</Text>
{tagItems}
</View>
)
}
Here is a full working code. Since your labels state property is an object, you need to map it somehow. I've chosen Object.values here. You can use Object.keys or even Object.entries according to your needs.
I've used a separate Info component and passed the values to it, then render there. In this component, we are again mapping the tags, then rendering the list.
class App extends React.Component {
state = {
labels: {
info1: {
label: "label1",
class: "class-css",
title: "title",
text: "text",
number: "20",
tags: [
{
name: "#twitter",
},
{
name: "#myspace",
},
],
},
info2: {
label: "label2",
class: "class-css",
title: "title",
text: "text",
number: "20",
tags: [
{
name: "#instagram",
},
{
name: "#facebook",
},
],
},
},
}
render() {
const { labels } = this.state;
return (
<div>
{
Object.values( labels ).map( value =>
<Info label={value} key={value.label} /> )
}
</div>
);
}
}
const Info = ( props ) => {
const { title, text, tags } = props.label;
const tagList = tags.map( tag => <p key={tag.name}>{tag.name}</p> );
return (
<div style={{ border: "1px solid gray", marginTop: "-1px" }}>
<p>{title}</p>
<p>{text}</p>
<div>{tagList}</div>
</div>
);
};
ReactDOM.render(
<App />,
document.getElementById("root")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Update
If your data is totally static then #Xavi A.'s method is a good option. I don't know how is your list but I provide a simple code including something like you want here.
const labels = {
info1: {
label: "label1",
class: "class-css",
title: "title",
text: "text",
number: "20",
tags: [
{
name: "#twitter"
},
{
name: "#myspace"
}
]
},
info2: {
label: "label2",
class: "class-css",
title: "title",
text: "text",
number: "20",
tags: [
{
name: "#instagram"
},
{
name: "#facebook"
}
]
}
};
class App extends React.Component {
state = {
currentLabel: Object.keys(labels)[0]
};
handleInfoChange = info => this.setState({ currentLabel: info });
renderList = () => (
<ul>
{Object.keys(labels).map(info => (
<Item key={info} info={info} onClick={this.handleInfoChange} />
))}
</ul>
);
render() {
const { currentLabel } = this.state;
return (
<div>
{this.renderList()}
<Info currentLabel={currentLabel} />
</div>
);
}
}
const Item = props => {
const { info, onClick } = props;
const handleClick = () => onClick(info);
return <li onClick={handleClick}>{info}</li>;
};
const Info = props => {
const { currentLabel } = props;
const { title, text, tags } = labels[currentLabel];
const tagList = tags.map(tag => <p key={tag.name}>{tag.name}</p>);
return (
<div style={{ border: "1px solid gray", marginTop: "-1px" }}>
<p>{title}</p>
<p>{text}</p>
<div>{tagList}</div>
</div>
);
};
ReactDOM.render( <App />, document.getElementById( "root" ) );
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
Probably something like this.
<Text>{infoDetail.tags.map(tag => {/*render */})}</Text>
You can try Object.keys() and Array.prototype.reduce() to get your favorite data:
const data = {
"info1": {
"label": "label",
"class": "class-css",
"title": "title",
"text": "text",
"number": "20",
"tags": [
{
"name": "#twitter"
},
{
"name": "#myspace"
}
]
},
"info2": {
"label": "label",
"class": "class-css",
"title": "title",
"text": "text",
"number": "20",
"tags": [
{
"name": "#instagram"
},
{
"name": "#facebook"
}
]
}
};
const tags = Object.keys(data).reduce((result, key) => {
return result.concat(data[key].tags);
}, [])
console.log(tags);
/* tags = [
{
"name": "#twitter"
},
{
"name": "#myspace"
},
{
"name": "#instagram"
},
{
"name": "#facebook"
}
] */
No need to save all the static data in your state, you can keep your state cleaner by just saving the selected label:
onLabelSelect = label => {
//label will be "info1" for example
this.setState({
currentLabel: label
})
}
Then in your render:
render(){
//get infoDetail from staticData
const infoDetail = staticData[this.state.currentLabel]
return (
<View>
<Text>{infoDetail.title}</Text>
<Text>{infoDetail.text}</Text>
{infoDetail.tags.map( ({name}) => <Text>name</Text>)}
</View>
)
}
Note about the map. This:
{infoDetail.tags.map( ({name}) => <Text>name</Text>)}
is a shorter version of:
{infoDetail.tags.map( item => {
return <Text>item.name</Text>
})}

Getting .filter to work with .map from JSON object in React.Js

I am trying to get my second dropdown to populate based on what is selected in the first dropdown.
The first dropdown lists the tableNames from the JSON below. I want the second dropdown to be columns for the selected tableName.
For now though I just wanted to see a list of the columns where tableName == 'customer'.
I am trying to use the .filter method along with the .map method to accomplish this, but the second dropdown is empty. Here is the current code I have:
import React from 'react';
import axios from 'axios';
class Search extends React.Component {
constructor(props) {
super(props)
this.state = {
Params: [],
ColumnParams: []
}
}
componentWillMount() {
axios.get('http://localhost:3010/api/schema')
.then(response => {
this.setState({Params: response.data})
})
}
render() {
return (
<div className="Search">
<select>{this.state.Params.map((param, i) =>
<option key={i}>{param.tableName}</option>)}
</select>
<select>{this.state.Params.filter(tn => tn === "customer").map(param =>
<option>{param}</option>)}
And here is the JSON:
[
{
"tableName": "customer",
"columns": [
"customerid",
"title",
"prefix",
"firstname",
"lastname",
"suffix",
"phone",
"email"
]
},
{
"tableName": "product",
"columns": [
"productid",
"name",
"color",
"price",
"productadjective",
"productmaterial"
]
}
]
Params in your state is an array of objects but in your filter you are trying to compare it with a string.
Try
.filter(({tableName}) => tableName === 'customer')
.map(({columns}) =>
columns.map((col) =>
<option>{col}</option>))}
instead.
A potentially better solution is to use reduce:
.reduce((options, { tableName, columns }) => {
if (tableName === 'customer') options.push(<option>{columns}</option>)
return options
}, [])
Given the JSON:
[
{
"tableName": "customer",
"columns": [
"customerid",
"title",
"prefix",
"firstname",
"lastname",
"suffix",
"phone",
"email"
]
},
{
"tableName": "product",
"columns": [
"productid",
"name",
"color",
"price",
"productadjective",
"productmaterial"
]
}
]
Your comparison in the filter method is wrong, you can change to use destructuring assignment:
import React from 'react';
import axios from 'axios';
class Search extends React.Component {
constructor(props) {
super(props)
this.state = {
params: [],
columnParams: []
}
}
componentWillMount() {
axios
.get('http://localhost:3010/api/schema')
.then(response => this.setState({params: response.data}))
}
render() {
const tableNames = this.state.params
.map(({tableName}, i) => <option key={i}>{tableName}</option>);
const params = this.state.params
.filter({tableName} => tableName === "customer")
.map({columns} => <option>{columns}</option>);
return (
<div className="Search">
<select>{tableNames}</select>
<select>{params}</select>
</div>
)
}
}
You can see more details here.

Categories