I am dynamically adding and removing Dropdowns based on the index of the array. I am setting the index using. When adding or removing a dropdown, I increment and decrement that index. The goal would be an array that looks something like this:
[
{ index: 0, type: "facebook" },
{ index: 1, type: "instagram" }
]
The problem is when I add a new dropdown, the index is incrementing by 2 or 3 instead of 1, resulting in the following output.
[
{ index: 0, type: "facebook" },
{ index: 2, type: "instagram" }
]
Here is the code for my component:
class SocialProfileComponent extends Component {
constructor(props) {
super(props);
let index = 0;
this.state = {
options: [
{value: 'email', label: 'Email'},
{value: 'instagram', label: 'Instagram'},
{value: 'linkedin', label: 'Linkedin'},
{value: 'pinterest', label: 'Pinterest'},
{value: 'skype', label: 'Skype'},
{value: 'tiktok', label: 'TikTok'},
{value: 'tumblr', label: 'Tumblr'},
{value: 'twitter', label: 'Twitter'},
{value: 'whatsapp', label: 'WhatsApp'},
{value: 'wordpress', label: 'Wordpress'},
{value: 'youtube', label: 'Youtube'},
{value: 'other', label: 'Other'},
],
socialDetails: [
{
index: index,
type: "",
link: "",
}
]
};
}
addNewRow = (e) => {
this.setState(prevState => ({
socialDetails: [
...prevState.socialDetails,
{
index: index++,
type: "",
link: "",
}
]
}));
}
deleteRow = (index) => {
this.setState({
socialDetails: this.state.socialDetails.filter(
(s) => index !== s.index
)
})
}
clickOnDelete(record) {
this.setState({
socialDetails: this.state.socialDetails.filter(r => r!==record)
})
}
componentDidMount() {
}
render = () => {
let {socialDetails, options} = this.state;
const {onInputChange} = this.props;
return (
<>
<SocialProfile
label={`Social profiles`}
addRow={this.addNewRow}
deleteRow={this.deleteRow}
socialDetails={socialDetails}
options={options}
onInputChange={onInputChange}
formKey={'socialprofiles'}
createKeyValue={this.createNewKeyValuePair}
placeholder={'Select'}
/>
</>
)
}
}
Code Sandbox
The behavior you are experiencing where the index increases by 2 or 3 is a result of React strict mode. In addition to other things, strict mode helps detect unexpected side effects to help you prepare your app for the upcoming concurrent mode feature. In concurrent mode, React will break up rendering into smaller chunks pausing and resuming work as necessary. This means that render phase lifecycle methods can be run more than once.
To help you prepare for this upcoming behavior of concurrent mode, strict mode will intentionally invoke render phase lifecycles twice to identify potential side effects. State updater functions are one instance of this, meaning that calling index++ in your state updater function will be run twice in strict mode.
The easiest solution would be to simply assign the new index to a variable before calling this.setState so that your state updater function is idempotent and can be called more than once.
addNewRow = (e) => {
const newIndex = ++this.index
this.setState((prevState) => ({
socialDetails: [
...prevState.socialDetails,
{
index: newIndex,
type: "",
link: ""
}
]
}));
};
Related
First, let me explain what I want to do.
I have tree-like data structure, with arbitrary depth from which i want to make a list of all non-leaf branches which I pass as props to Child component.
Data set and recursive function are bellow.
Problem:
I get double data in array! I noticed that function getBranches() is called also in chrome VM and it just appends same data again to already existing array. See pic bellow.
Is my approach fundamentally wrong? What to do?
const treeData = {
'first-level-node-1': { // key
label: 'Regija',
type: TYPES[1],
index: 0, // rendering order
url: 'http://opa.com',
nodes: {
'second-level-node-1': {
label: 'Bolnica BN',
index: 0,
type: 1,
nodes: {
'third-level-node-1': {
label: 'Konektor Sysmex',
index: 0,
nodes: {},
type: TYPES[0]
},
},
},
},
},
'first-level-node-2': {
label: 'Regija 2',
index: 1,
type: TYPES[1],
nodes: {
'2-1': {
label: 'Dz Trebinje',
index: 0,
type: 1,
nodes: {
'3-1': {
label: 'Konektor Biomerux',
index: 0,
hasNodes: false,
type: TYPES[0],
}
}
}
}
},
};
My getBranches() function:
const getBranches = (treeData) => {
console.log('i= ',i++)
Object.keys(treeData).forEach((key) => {
if (treeData[key].type!==TYPES[0]) {
branches.push({key: key, label: treeData[key].label});
console.log({key: key, label: treeData[key].label})
treeData[key].nodes && getBranches(treeData[key].nodes)
};
});
return [...branches];
};
My Component:
const Sites = () => {
const branches = getBranches(treeData);
console.log(branches);
//const [data, setData] = useState(treeData);
return (
<Container fluid="md">
<Row>
<Col>
<MyTreeView data={treeData}/>
</Col>
<Col>
<BranchView
sites={treeData}
parentList={branches}
/>
</Col>
</Row>
</Container>
);
};
Some additional info:
branch vs leaf is distinguished by property type
The Picture. Notice the i s
I'm guessing the problem is simply that you don't declare branches inside your function, making it a global variable. So the next time the function is run, you're appending to it. Adding a const declaration should probably do.
That said, I think you can simplify getBranches in a clean manner:
const getBranches = (o) => Object .entries (o) .flatMap (
([key, {label, type, nodes}]) => [
... (type == TYPES [0] ? [] : [{key, label}]),
... (nodes ? getBranches (nodes) : [])
]
)
const TYPES = [0, 1]
const treeData = {"first-level-node-1": {label: "Regija", type: TYPES[1], index: 0, url: "http: //opa.com", nodes: {"second-level-node-1": {label: "Bolnica BN", index: 0, type: 1, nodes: {"third-level-node-1": {label: "Konektor Sysmex", index: 0, nodes: {}, type: TYPES[0]}}}}}, "first-level-node-2": {label: "Regija 2", index: 1, type: TYPES[1], nodes: {"2-1": {label: "Dz Trebinje", index: 0, type: 1, nodes: {"3-1": {label: "Konektor Biomerux", index: 0, hasNodes: false, type: TYPES[0]}}}}}}
console .log (getBranches (treeData))
.as-console-wrapper {max-height: 100% !important; top: 0}
I am trying to implement 2 select box
1st select box will be the simple select box.
2nd select box will have infinite scroller functionality and for this, I am using the react-select-async-paginate library.
Issue Explanation
AsyncPaginate is the component of react-select-async-paginate library. It uses loadOptions function attribute to load the option into the select box and it expect return value as {options: [], hasMore: false}.
In my code, in the loadOptions attribute, I am calling the loadHostOptions function to get the options. And on change of the first dropdown, I am calling loadHostOptions function. But in this case, correct options are not reflected in the 2nd dropdown.
Can anyone help me with how to load options on the change of the first dropdown?
Here is codesandbox
Code
import React from "react";
import { AsyncPaginate } from "react-select-async-paginate";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
firstSelectVal: "",
value: null
};
}
firstSelectChange = (event) => {
this.setState({ firstSelectVal: event.target.value });
if (event.target.value) {
this.loadHostOptions("java", []);
}
};
onPagiChange = (event) => {
this.setState({ value: event});
};
loadHostOptions = async (search, prevOptions) => {
if (search === "java") {
const responseJSON = {
results: [
{
value: 1,
label: "Java"
}
],
has_more: false
};
return {
options: responseJSON.results,
hasMore: responseJSON.has_more
};
} else {
const responseJSON = {
results: [
{
value: 1,
label: "Java"
},
{
value: 2,
label: "C++"
},
{
value: 3,
label: "Python"
},
{
value: 4,
label: "Node"
},
{
value: 5,
label: "Go, Lang"
}
],
has_more: false
};
return {
options: responseJSON.results,
hasMore: responseJSON.has_more
};
}
};
render() {
return (
<div>
<h1>react-select-async-paginate</h1>
<h2>1st Selectbox</h2>
<select
id="lang"
onChange={this.firstSelectChange}
value={this.state.firstSelectVal}
>
<option value="">Select</option>
<option value="java">Java</option>
</select>
{this.state.firstSelectVal ? (
<>
<h2>2nd Selectbox</h2>
<AsyncPaginate
value={this.state.value}
loadOptions={(search, prevOptions) =>
this.loadHostOptions(search, prevOptions)
}
onChange={(e) => this.onPagiChange(e)}
/>
</>
) : (
""
)}
</div>
);
}
}
export default App;
You will have to change your onPagiChange function to
onPagiChange = (event) => {
this.setState({ value: event});
};
That is set the entire event as value instead of event.value
Update
The actual issue is whenever you click on the AsyncPaginate field it will call loadHostOptions function and pass the current value to the search argument. In that case whatever you type in will be the value of search.
So if you want to use the value of first Select box to filter option for the second one, you will have to directly use the this.state.firstSelectVal directly inside the loadHostOptions function. Like this
loadHostOptions = async (search, prevOptions) => {
if (this.state.firstSelectVal === "java") {
const responseJSON = {
results: [
{
value: 1,
label: "Java"
}
],
has_more: false
};
return {
options: responseJSON.results,
hasMore: responseJSON.has_more
};
} else {
const responseJSON = {
results: [
{
value: 1,
label: "Java"
},
{
value: 2,
label: "C++"
},
{
value: 3,
label: "Python"
},
{
value: 4,
label: "Node"
},
{
value: 5,
label: "Go Lang"
}
],
has_more: false
};
return {
options: responseJSON.results,
hasMore: responseJSON.has_more
};
}
};
I have an array that looks like this:
const MENUS_LIST: MenuListing[] = [
{
id: 1,
name: 'Analytics',
defaultPath: '/analytics/sales/overview',
relatedPath: ['/analytics/sales/overview', '/analytics/sales/bookings'],
submenu: [
{
label: 'Overview',
path: '/analytics/sales/overview',
relatedPath: ['/analytics/sales/overview'],
additionalIcon: [],
name: ['Overview'],
id: 'sales-overview',
},
{
label: 'Bookings',
path: '/analytics/sales/bookings',
relatedPath: ['/analytics/sales/bookings'],
additionalIcon: [],
name: ['Bookings'],
id: 'sales-bookings',
},
],
},
];
I need to convert it to the following format - by adding the isActive flag to the main structure and submenu when the current path === submenu.path.
In the following example, we assume path to be /analytics/sales/overview.
[
{
id: 1,
name: 'Analytics',
defaultPath: '/analytics/sales/overview',
relatedPath: ['/analytics/sales/overview', '/analytics/sales/bookings'],
isActive: true,
submenu: [
{
label: 'Overview',
path: '/analytics/sales/overview',
relatedPath: ['/analytics/sales/overview'],
additionalIcon: [],
name: ['Overview'],
id: 'sales-overview',
isActive: true,
},
{
label: 'Bookings',
path: '/analytics/sales/bookings',
relatedPath: ['/analytics/sales/bookings'],
additionalIcon: [],
name: ['Bookings'],
id: 'sales-bookings',
isActive: false,
},
],
},
];
I have the following solution which works (code is simplified):
menuX = (MENUS_LIST as MenuListingProps[]).map((m: MenuListingProps) => {
const resultX = { ...m };
resultX.isActive = true; // will perform checking to determine true or false
(m.submenu as MenuItemProps[]).forEach((sm: MenuItemProps) => {
sm.isActive = true; // linter warning; value assigned based on checking (can be true or false)
sm.icon = 'abc'; // linter warning
sm.title = 'xyz'; // linter warning
});
return resultX;
});
But the linter is complaining of Assignment to property of function parameter "sm" on the lines where I'm assigning values to sm
Based on this SO post, I understand that I need to copy the argument to a temporary variable and work on it instead.
I did this by creating a new var resultX. But I'm not sure how to go about doing the same with sm.
Seeking some guidance, thank you.
menuX = (MENUS_LIST as MenuListingProps[]).map((m: MenuListingProps) => {
const resultX = { ...m };
resultX.isActive = true; // will perform checking to determine true or false
resultX.submenu = (m.submenu as MenuItemProps[]).map((sm: MenuItemProps) => {
const sub = {...sm};
sub.isActive = true; // linter warning; value assigned based on checking (can be true or false)
sub.icon = 'abc'; // linter warning
sub.title = 'xyz'; // linter warning
return sub;
});
return resultX;
});
Here's a method using Object.assign.
Object.assign doesn't mutate the original object, so it returns a new object with the given changes.
const MENUS_LIST = [{"id":1,"name":"Analytics","defaultPath":"/analytics/sales/overview","relatedPath":["/analytics/sales/overview","/analytics/sales/bookings"],"submenu":[{"label":"Overview","path":"/analytics/sales/overview","relatedPath":["/analytics/sales/overview"],"additionalIcon":[],"name":["Overview"],"id":"sales-overview"},{"label":"Bookings","path":"/analytics/sales/bookings","relatedPath":["/analytics/sales/bookings"],"additionalIcon":[],"name":["Bookings"],"id":"sales-bookings"}]}];
const menuX = MENUS_LIST.map(menu => Object.assign(menu, {
isActive: true, // will perform checking to determine true or false
submenu: menu.submenu.map(submenu => Object.assign(submenu, {
isActive: true, // will perform checking to determine true or false
icon: 'abc', // linter warning
title: 'xyz' // linter warning
}))
}));
console.log(menuX);
I hope this code helping you
var array = [
{
id: 1,
name: 'Analytics',
defaultPath: '/analytics/sales/overview',
relatedPath: ['/analytics/sales/overview', '/analytics/sales/bookings'],
isActive: true,
submenu: [
{
label: 'Overview',
path: '/analytics/sales/overview',
relatedPath: ['/analytics/sales/overview'],
additionalIcon: [],
name: ['Overview'],
id: 'sales-overview',
isActive: true,
},
{
label: 'Bookings',
path: '/analytics/sales/bookings',
relatedPath: ['/analytics/sales/bookings'],
additionalIcon: [],
name: ['Bookings'],
id: 'sales-bookings',
isActive: false,
},
],
},
];
array.map(o=>o.submenu.map(v=> o.defaultPath == v.path? {...v,isActive:true,icon:"abc",title:'xyz'}:{...v,isActive:false,icon:"abc",title:'xyz'}))
I have following state:
merchant: [{
title: "Setup",
steps: [
{id: "provider", done: false},
{id: "api_key", done: false}
{id: "client", done: false}
]
}]
and i want to update it with the following dataset
merchant: [{
title: "Setup",
steps: [
{id: "provider", done: false},
{id: "api_key", done: true}
]
}]
So that I end up with the following:
merchant: [{
title: "Setup",
steps: [
{id: "provider", done: false},
{id: "api_key", done: true}
{id: "client", done: false}
]
}]
What would be the cleanest way to achieve this?
I've done something like this in my reducer, but it seems like a terrible idea based on the output I'm getting.
guide_progression: {
...state.guide_progression,
merchant: state.guide_progression.merchant.map(stateGuide =>
payload.user.guide_progression.merchant.map(userGuide =>
userGuide.title === stateGuide.title &&
{
...stateGuide,
steps: stateGuide.steps.map(stateStep =>
userGuide.steps.map(userStep =>
userStep.id === stateStep.id &&
{
...stateStep,
done: userStep.done
}
)
)
}
)
)
}
Really appreciate suggestions for how to solve this. I've been struggling to find a good solution on the web.
You can use Immer, It allows you to
Create the next immutable state tree by simply modifying the current tree
Basically allows you to modify your data while keeping it immutable.
So immer is straight up amazing. Thanks ahmed mahmoud. Here's the solution I ended up with using immer.js
updateMerchantState = produce(state.guide_progression.merchant, draft => {
payload.user.guide_progression.merchant.map(userGuide => {
const guideIndex = draft.findIndex(guide => guide.title === userGuide.title)
if (guideIndex !== -1) {
userGuide.steps.map(userStep => {
const stepIndex = draft[guideIndex].steps.findIndex(step => step.id === userStep.id)
if (stepIndex !== -1) draft[guideIndex].steps[stepIndex].done = userStep.done
})
}
})
})
How do I change property of item in ImmutableList({}) that is inside Immutable.Map({})?
I have:
const initialState = Immutable.Map({
width: window.board.width,
height: window.board.height,
lines: Immutable.List([
Immutable.Map({id: 1, active: false, name: 'Some name'}),
Immutable.Map({id: 2, active: false, name: 'Sad name'}),
Immutable.Map({id: 3, active: true, name: 'Cool name'})
])
});
Lets say I want to set item with id 1 in the List. Then change its property active to true. I also want to set property active to false for all the other items in the List.
How do I do that? Thanks a lot in advance.
Edit (final solution):
export function activateLine(lineId) {
return function (dispatch, getState) {
const updatedLines = getState().board.update('lines', Immutable.List(),
(oldList) => oldList.map((listItem) => {
return listItem.set("active", (listItem.get("id") === lineId));
})).get('lines');
return dispatch({
type: types.ACTIVATE_LINE,
payload: updatedLines
});
};
}
You can traverse immutable like so (quick note though i would highly recommend you make the entire object immutable). You can keep down the lines of code and keep it a bit more elegant by just using immutable itself -
const changeImmutableObject = initialState.update('lines', Immutable.List(),
(oldList) => oldList.map((listItem) => {
if(listItem.get("id") === 1) {
return listItem.set("active", true);
} else {
return listItem.set("active", false);
}
})
)
See working fiddle - https://jsfiddle.net/o04btr3j/214/
This updates the list key inside your object (if it is not there for whatever reason it defaults to an immutable list), then maps over the properties. If it has an id of 1, it will set active to true, otherwise it will set active to false.
You can also just initialState.toJS() your immutable object and do your logic in plain js, or do and immutableobject.get('property') and modify and then set that back in your object, however I would recommend you just use the built in methods in immutable as it is much less code.
Also, If I'm not mistaken you can just do something like this :
const initialState = Immutable.fromJS({
width: window.board.width,
height: window.board.height,
lines:[
{id: 1, active: false, name: 'Some name'},
{id: 2, active: false, name: 'Sad name'},
{id: 3, active: true, name: 'Cool name'}
]
});
You can immutable.fromJS() an object to put it into an immutable object. And all immutable objects should have a .toJS method on them to turn them back into regular javascript objects/lists/whatever.
I have created lithe example on link to solution on jsbin.com
const initialState = Immutable.Map({
lines: Immutable.List([
Immutable.Map({id: 1, active: false, name: 'Some name'}),
Immutable.Map({id: 2, active: false, name: 'Sad name'}),
Immutable.Map({id: 3, active: true, name: 'Cool name'})
])
});
console.log('initialState', initialState.toJS())
// update lines, map over items and set active to false
const stateLinesAllNotActive = initialState.updateIn(
['lines'],
items => items.map((item) => item.set('active', false))
)
console.log('stateLinesAllNotActive', stateLinesAllNotActive.toJS())
// lines and key for item with id === 1, then set active to this item
const stateFirstActive = stateLinesAllNotActive.updateIn(
[
'lines',
stateLinesAllNotActive.get('lines').findIndex((item) => (item.get("id") === 1))
],
item => item.set("active", true)
)
console.log('stateFirstActive', stateFirstActive.toJS())
What follows should work, but, as someone said... I think could be better a full update...
const initialState = Immutable.Map({
lines: Immutable.List([
Immutable.Map({id: 1, active: false, name: 'Some name'}),
Immutable.Map({id: 2, active: false, name: 'Sad name'}),
Immutable.Map({id: 3, active: true, name: 'Cool name'})
])
});
let lines = initialState.get('lines');
let id = 1; // what you want update...
let updater = item => {
let updated = item.set('name', "Giuseppe");
return updated;
};
let index = lines.findIndex((i) => i.get("id") === id);
let result = lines.update(index, updater);
initialState.set("lines", result);
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.js"></script>