First of all this is a simplified example: Codepen Project
I am building an edit form in react, which checks if there are any changes.
You can only save the form if there are any changes and any changes you made will be shown by changing the style (border-left) on the matching input element. It looks like this
To do that I am saving the original data/ state in the component state in the componentDidMount method and compare that to the state of the different inputs.
componentDidMount() {
// if the project is accessed from home and is not a new project, project data will be passed along
if (this.props.project) {
this.setState({
name: this.props.project.name,
tags: this.props.project.tags
}, this.setInitialState)
} else if (this.props.edit && this.props.match.params.id) {
// instead of an api call to get project data, if the project is accessed directly by url
const project = projects.find((project) => project.name === this.props.match.params.id)
this.setState({
name: project.name,
tags: project.tags
}, this.setInitialState)
}
// if there are no project data or an edit prop, it's a new project and the initialState remains empty
}
On each Input Change the input Value is compared to the initialState:
compareInputData() {
const formFields = {
name: {
ref : this.name,
changed : false
},
tags: {
ref : this.tagList,
changed : false
}
}
const state = this.state
const first = this.state.initialState
const nameHasChanged = state.name !== first.name
const tagsHaveChanged = state.tags.length !== first.tags.length
nameHasChanged
? ( formFields.name.changed = true )
: ( formFields.name.changed = false )
tagsHaveChanged
? ( formFields.tags.changed = true )
: ( formFields.tags.changed = false )
nameHasChanged || tagsHaveChanged
? (this.setState({
isChanged: true
}))
: (this.setState({
isChanged: false
}))
this.handleChangedInputStyles(formFields)
}
If there are changes the styling of the matching element is changed:
handleChangedInputStyles(formFields) {
const formFieldKeys = Object.keys(formFields)
formFieldKeys.map(key => {
formFields[key].changed
? formFields[key].ref.style.borderLeft = `2px solid orange`
: formFields[key].ref.style.borderLeft = '1px solid black'
})
}
That is working the way I want it to on normal input fields, but I am also saving related tags as an array, which are displayed as a list. Whenever I update that list (this.state.tags) my original state for the tags is being updated as well (this.state.initialState.tags), which means that I cannot pick up changes in my tag List.
However it does work if I am creating adding a tag to a new project instead of editing an existing one...
I have no idea how to fix that issue, since I don't really know what's causing it and I would love some help.
Thank you for reading through this post :)
Do not store this.state.initialState in the state. Store it in a member instead. For instance:
constructor(props) {
this.initialState = Object.assign({}, whatever...);
this.initialState.tags = [].concat(this.initialState.tags); // Keep a shallow copy of this array.
}
Note: Internally, React may modify the tags array. If you keep a copy, that copy will not be modified.
Related
I have started to work on a form project using React final form, but I am struggling to do a two condition question.
Would anyone knows how to do a double conditional logic on React final form. From this example, the single conditional logic code is given :
const Condition = ({ when, is, children }) => ( <Field name={when} subscription={{ value: true }}>
{({ input: { value } }) => (value === is ? children : null)} </Field>)
However, I don't know how to do a double conditional one that would require 2 different answers from two different questions in the form to be checked before displaying the conditional question.
Thank you :)
You could use useFormState from react-final-form to obtain current form state, so MultiCondition component could look like this:
const MultiCondition = ({ condition, children }) => {
const { values = {} } = useFormState({ subscription: { values: true } });
return condition(values) ? children : null;
};
than somewhere inside the form:
<MultiCondition condition={({ gift, firstName }) => gift && firstName === "Joe"}>
<span>gets rendered when condition is satisfied</span>
</MultiCondition>
There is a component that maps through an array stored in the state. A button, when it is clicked it updates the state, this action is working.
The problem is that the component is not updating too.
Here is the code:
const MyComponent = () => {
...
const [fields, setFields] = useState([{value: 'test', editable: false},
{value: 'test2', editable: false}]);
...
const toggleClass = (id) => {
const aux = fields;
aux[id].editable = true;
setFields(aux);
}
...
return (
<div>
...
{fields.map((field, id) => {
return (
<div>
<input className={field.editable ? 'class1' : 'class2'} />
<button onClick={() => toggleClass(id)}>click</button>
</div>
);
})}
</div>
);
I put logs and the state (fields) is updated after click to editable = true. But the css class is not changing.
Is there any solution to this issue?
You need to make a copy of your existing state array, otherwise you're mutating state which is a bad practice.
const toggleClass = id => {
const aux = [...fields]; //here we spread in order to take a copy
aux[id].editable = true; //mutate the copy
setFields(aux); //set the copy as the new state
};
That's happening because you are mutating the value of fields, which makes it unsure for React to decide whether to update the component or not. Ideally if you should be providing a new object to the setFields.
So, your toggleClass function should look like something below:
const toggleClass = (id) => {
const aux = [...fields]; //This gives a new array as a copy of fields state
aux[id].editable = !aux[id].editable;
setFields(aux);
}
BTW, I also noticed that you're not assigning a key prop to each div of the the map output. Its a good practice to provide key prop, and ideally keep away from using the index as the key.
I created to do list using react, but I want it to be local storage - so when the user refresh the page it still saved the items and will present them.
I read I need to use localStorage but I'm not sure where and how, attach the app.js and TodoItem component
class App extends Component {
state = {
items: [],
id: uuidv4(),
item: "",
editItem: false
};
handleChange = e => {
...
};
handleSubmit = e => {
e.preventDefault();
const newItem = {
id: this.state.id,
title: this.state.item
};
const updatedItems = [...this.state.items, newItem];
this.setState({
items: updatedItems,
item: "",
id: uuidv4(),
editItem: false
});
};
...
render() {
return (
<TodoInput
item={this.state.item}
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
editItem={this.state.editItem}
/>
<TodoList
items={this.state.items}
clearList={this.clearList}
handleDelete={this.handleDelete}
handleEdit={this.handleEdit}
/>
);
}
}
export default class TodoItem extends Component {
state = {
avatarURL: '',
}
componentDidMount() {
imgGen().then(avatarURL => this.setState({ avatarURL }));
}
render() {
const { title, handleDelete, handleEdit } = this.props;
const { avatarURL } = this.state;
return (
<h6>{title}</h6>
<span className="mx-2 text-success" onClick={handleEdit}>
</span>
<span className="mx-2 text-danger" onClick={handleDelete}>
</span>
);
}
}
You can do it like this, mind the comments
class App extends Component {
state = {
// load items while initializing
items: window.localStorage.getItem('items') ? JSON.parse(window.localStorage.getItem('items')) : [],
id: uuidv4(),
item: "",
editItem: false
};
handleChange = e => {
// ...
};
handleSubmit = e => {
e.preventDefault();
const newItem = {
id: this.state.id,
title: this.state.item
};
const updatedItems = [...this.state.items, newItem];
// Save items while changing
window.localStorage.setItem('items', JSON.stringify(updatedItems));
this.setState({
items: updatedItems,
item: "",
id: uuidv4(),
editItem: false
});
};
// ...
render() {
return (
<>
<TodoInput
item={this.state.item}
handleChange={this.handleChange}
handleSubmit={this.handleSubmit}
editItem={this.state.editItem}
/>
<TodoList
items={this.state.items}
clearList={this.clearList}
handleDelete={this.handleDelete}
handleEdit={this.handleEdit}
/>
</>
);
}
}
Here's some simple logic you can use in your componentDidMount() method of your App.
const localStorageList = localStorage.getItem('todo-list')
if (!localStorageList) {return null} else {this.setState({items: localStorageList})
To add to the localStorage please look at this question
and this resource
Let me help you with this, using the least no. of codes. I have written a clear explanation of the steps, for you all to better understand, please bear with me , it is definitely with the time to read.
Also, note this solution is perfectly crafted for functional components. However I have mentioned how to do it in class components, you have to tweak some things if you are using class components. Like you can not use hooks in class-based components, but access this instance, so it will be fine, either ways
Please give it a full read, if you are having a tough time understanding the functionality, I have tried to break down the process in layman. The explanation is long, the lines of code is just under 10. happy to help
Persisting states of the todo app, upon page refresh, is pretty simple.
We can use State management libraries for it, or local storage as well.
Here, we will just go with the most simple one - using local storage.
Before we jump to the code, let us build the functionality visually.
So, after the user enters things in the todo space, we want few things to happen:
We want to store the list of items (which will essentially be an array) in the local storage. (We can skip the JSON.parse, here, since the array that will be saved, will be string, bcz user enters string in the todo-app, generally, however, it's not a bad idea to parse the userinputs).
useEffect(()=>{
window.localStorage.setItems("key" , value)
}, [dependency])
After you do this, make sure you check the dev-tools => application => localStorage => to see if the key and values are being stored. You shall be able to see them.
However, you will notice, that upon refresh, the localStorage values stay, but the data in the UI are lost. Not surprising.
This is the last and important step.
What we want upon page reload? Let us break it down :
We want to check if there is any data that is there in the localStorage. If there is: we will change the state of the app, based on the previous user inputs.
If there is no data in the LocalStorage, we will just pass an empty array.
Using hooks, in the functional component is actually What I prefer, class components require many boiler plates, so, the code...
import {useState} from 'react';/for functional components
//for class components you can just init the state , in the constructor(props) and
change it using the this.setState method
//to getItems from localStorage to render in the UI
useEffect(()=>{
const storedData = localStorage,getItems("keys" , value)
storedData ? setValue(value) : [];
},[])
[] : because we want it to render on every reload, once.
smake sure to initiliaze the state using useState;
const [value , setValue] = useState("")
//to setItems in localStorage
useEffect(()=>{
window.localStorage.setItems("key" , value)
}, [dependency])
useEffect is essentially a hook for functional components which is similar to componentDidMount in-class components.
If using class components, instead of using the useState, hook, use this.setState.
You could format your todolist into a JSON string and store it using :
localStorage.setItem("todolist", "your_JSON_string_here");
However, web Local Storage have storage limitations which will cause issues if the data stored are getting larger in time.
More info at here
Perhaps you could consider IndexedDB (if you are storing huge data) INFO
As you can see in below screencast, manipulating the second input field in my form also changes the value of the first one. I've completely reproduced it on JSfiddle in an isolated environment to track down the bug but didn't find it.
Quick overview of my code base:
A functional component ModuleBuilder has an Array in its state called blocks and a renderFunction which renders for each element of blocks one instance of a subcomponent called ModuleElement.
The subcomponent ModuleElement displays an input form element. When I create n instances of the subcomponent and try to type something in the the form element of the second instance then the first one's value gets updated as well.
const ModuleElement = (props) => {
const blockElement = props.config;
const key = props.index;
const changeValue = (e) => {
blockElement[e.target.name] = e.target.value;
props.updateBlock(key, blockElement);
};
return (
<div key={`block_${key}`}>
<input
key={`block_input_${key}`}
type="text"
id={`fieldname_${key}`}
name="fieldName"
onChange={changeValue}
placeholder="Field Name"
defaultValue={blockElement.fieldName}
/>
</div>
);
};
const ModuleBuilder = () => {
const emptyBlock = {
fieldName: "",
required: true,
};
const [blocks, setBlocks] = React.useState([emptyBlock]);
const updateBlock = (key, data) => {
//console.log(`i was asked to update element #${key}`);
//console.log("this is data", data);
let copyOfBlocks = blocks;
//copyOfBlocks[key] = data; // WHY DOES COMMENTING THIS OUT STILL MAKE THE UPDATE WORK??
setBlocks([...copyOfBlocks]);
// console.log("this is blocks", blocks);
};
const add1Block = () => {
setBlocks([...blocks, emptyBlock]);
};
const renderBlockFormElements = () => {
return blocks.map((value, index) => {
return (
<li key={index}>
<b>Subcomponent with Index #{index}</b>
<ModuleElement
index={index}
config={value}
updateBlock={updateBlock}
/>
</li>
);
});
};
return (
<div>
<h1>
Click twice on "add another field" then enter something into the second
field.
</h1>
<h2>Why is 1st field affected??</h2>
<form>
<ul className="list-group">{renderBlockFormElements()}</ul>
<button type="button" onClick={add1Block}>
Add another field
</button>
</form>
<br></br>
<h2>This is the state:</h2>
{blocks.map((value, index) => (
<p key={`checkup_${index}`}>
<span>{index}: </span>
<span>{JSON.stringify(value)}</span>
</p>
))}
</div>
);
};
ReactDOM.render(<ModuleBuilder />, document.querySelector("#app"));
see full code on https://jsfiddle.net/g8yc39Lv/3/
UPDATE 1:
I solved the problem (see my own answer below) but am still confused about one thing: Why is copyOfBlocks[key] = data; in the updateBlocks() not necessary to update the state correctly? Could it be that the following code is manipulating the props directly??
const changeValue = (e) => {
blockElement[e.target.name] = e.target.value;
props.updateBlock(key, blockElement);
};
If yes, what would be the react way to structure my use case?
UPDATE 2:
It turns out indeed that I was manipulating the props directly. I changed now the whole setup as follows.
The Module Element component now has its own state. OnChange the state is updated and the new target.value is pushed to the props.updateblocks method. See updated JSfiddle here. Is this the react way to do it ?
Suggested changes to your new answer:
Your updated answer is mostly correct, and more React-ish. I would change a couple things:
You have two state variables, which means you have two sources of truth. It works now because they're always in sync, but this has the potential to cause future bugs, and is not actually necessary. ModuleElement doesn't need a state variable at all; you can just render props.config.fieldName:
<input
...
onChange={(e) => {
props.updateBlock(key, {fieldName:e.target.value, required:true})
}}
value={props.config.fieldName}
/>
Then, you can eliminate the state variable in ModuleElement:
const ModuleElement = (props) => {
const key = props.index;
return (
<React.Fragment>
...
I would write updateBlock a little differently. copyOfBlocks is not actually a copy, so copyOfBlocks[key] = data; actually mutates the original data, and a copy is not made until setBlocks([...copyOfBlocks]);. This isn't a problem per se, but a clearer way to write it could be:
const updateBlock = (key, data) => {
let copyOfBlocks = [...blocks];
copyOfBlocks[key] = data;
setBlocks(copyOfBlocks);
};
Now, copyOfBlocks is actually a shallow copy ([...blocks] is what causes the copy), and not a pointer to the same data.
Here's an updated fiddle.
Answers to your remaining questions:
//copyOfBlocks[key] = data; // WHY DOES COMMENTING THIS OUT STILL MAKE THE UPDATE WORK??
Because this line doesn't actually copy:
let copyOfBlocks = blocks;
// This outputs `true`!
console.log(copyOfBlocks === blocks);
This means that, when you do this in ModuleElement - changeValue...
blockElement[e.target.name] = e.target.value;
... you're mutating the same value as when you do this in ModuleBuilder - updateBlock ...
copyOfBlocks[key] = data;
... because copyOfBlocks[key] === blocks[key], and blocks[key] === ModuleBuilder's blockElement. Since both updaters had a reference to the same object, you were updating the same object twice.
But beyond that, mutation of state variables in React is an anti-pattern. This is because React uses === to detect changes. In this case, React cannot tell that myState has changed, and so will not re-render the component:
const [myState, setMyState] = useState({'existing key': 'existing value'});
// ...
// This is BAD:
myState['key'] = ['value'];
const [myState, setMyState] = useState({'existing key': 'existing value'});
// ...
// This is still BAD, because the state isn't being copied:
const newState = myState;
newState['key'] = ['value'];
// At this point, myState === newState
// React will not recognize this change!
setMyState(newState);
Instead, you should write code that
performs a shallow copy on myState, so the old state !== the new state, and
uses setMyState() to tell React that the state has changed:
const [myState, setMyState] = useState({'existing key': 'existing value'});
// ...
// This is correct, and uses ES6 spread syntax to perform a shallow copy of `myState`:
const newState = {...myState, key: 'value'};
// Now, newState !== myState. This is good :)
setMyState(newState);
Or, as a one-liner:
setMyState({...myState, key: 'value'});
The Module Element component now has its own state. onChange the state is updated and the new target.value is pushed to the props.updateblocks method. Is this the react way to do it?
For the most part, yes. You're duplicating your state in two variables, which is unnecessary and more likely to cause bugs in the future. See above for a suggestion on how to eliminate the additional state variable.
I was able to solve the problem after coincidentally reading this question.
I replaced the setBlocks line here:
const emptyBlock = {
fieldName: "",
dataType: "string",
required: true,
};
const add1Block = () => {
setBlocks([
...blocks,
emptyBlock
]);
};
by this statement
const add1Block = () => {
setBlocks([
...blocks,
{ fieldName: "", dataType: "string", required: true },
]);
};
and it turned out that by placing emptyBlock as a default value for a new element I was just apparently just re-referencing it.
Prerquisite
I'm fetching a list of accounts (Ajax request) which I display on page load (with a checkbox next to them). By default all the accounts are selected and added to the store (redux).
Goal
Add/remove accounts from array & store (redux) when checkbox are checked/unchecked:
checbox is checked --> add account to array & store
checkbox is unchecked --> remove account from array & store
Logic
I created two separate actions & reducers:
one to manage the checkbox status
one to manage the addition/removal of the account to the array &
store
When testing my code, it works fine at the beginning but eventually the accounts added/removed are not correct. The issue must be in savingAccount() but not sure what I'm doing wrong?
My code
Pre-populating data to the store in ComponentWillMount():
componentWillMount = () => {
let defaultAccount = this.props.account
let defaultCheckbox = this.props.checkboxStatus
for(let i =0; i < this.props.products.arrangements.items.length; i++){
const data = {}
data['id'] = i
data['isSelected'] = true
data['sortCode'] = this.props.products.arrangements.items[i].sortCode
data['accountNumber'] = this.props.products.arrangements.items[i].accountNumber
data['accountName'] = this.props.products.arrangements.items[i].accountName
defaultAccount = defaultAccount.concat(data)
const checkboxesArray = {}
checkboxesArray['id'] = i
checkboxesArray['checked'] = true
defaultCheckbox = defaultCheckbox.concat(checkboxesArray)
}
this.props.addAccount(defaultAccount)
this.props.toggleCheckbox(defaultCheckbox)
}
Displaying list of accounts from Ajax response (this.props.products.arrangements.items)
render() {
return (
<div>
{typeof(this.props.products.arrangements.items) !== 'undefined' &&
(Object.keys(this.props.account).length > 0) &&
(typeof(this.props.checkboxStatus) !== 'undefined') &&
(Object.keys(this.props.checkboxStatus).length > 0) &&
(Object.keys(this.props.products.arrangements.items).length > 0) &&
<div>
{this.props.products.arrangements.items.map((item,i) =>
<div className="accountContainer" key={i}>
<Checkbox
required
label={"Account Number "+item.accountNumber+" Product Name "+item.accountName}
value={true}
checked={this.props.checkboxStatus[i].checked === true? true: false}
onChange = {(e) => {
this.toggleChange(this.props.checkboxStatus[i])
this.saveAccount(e, i, item.accountNumber, item.accountName)
}}
/>
</div>
)}
</div>
}
</div>
)
}
Updating isSelected value when checkbox is checked/unchecked:
saveAccount = (e, i, accountNumber, productName) => {
const data = {};
data['id'] = i
data['accountNumber'] = accountNumber
data['productName'] = productName
if(this.props.checkboxStatus[i].checked === true){
let accountArray = Array.from(this.props.account)
accountArray[i].isSelected = true
this.props.addAccount(accountArray)
}
else {
let accountArray = Array.from(this.props.account)
accountArray[i].isSelected = false
this.props.addAccount(accountArray)
}
}
Reducer
function Eligible(state = { products: {}, account: [], checkboxStatus: [] }, action){
switch (action.type){
case ADD_PRODUCTS:
return {
...state,
products: action.data
}
case ADD_ACCOUNT:
return {
...state,
account: action.data
}
case TOGGLE_CHECKBOX:
return {
...state,
checkboxStatus: action.data
}
default:
return state
}
}
Actions
export const ADD_PRODUCTS = 'ADD_PRODUCTS'
export const ADD_ACCOUNT = 'ADD_ACCOUNT'
export const TOGGLE_CHECKBOX = 'TOGGLE_CHECKBOX'
export function addProducts(data){
return {type: ADD_PRODUCTS, data}
}
export function addAccount(data) {
return { type: ADD_ACCOUNT, data}
}
export function toggleCheckbox(data) {
return { type: TOGGLE_CHECKBOX, data}
}
Updating checkbox status:
toggleChange = (checkbox) => {
let toggleCheckbox = this.props.checkboxStatus
toggleCheckbox[checkbox.id].checked = !checkbox.checked
this.props.toggleCheckbox(toggleCheckbox)
}
I think the asynchronicity of this.setState is probably causing an issue.
this.state contains both accounts and checkboxes:
this.state = {
accounts: [],
checkboxes: []
}
In your change event handler, you call two functions:
onChange = {(e) => {
this.toggleChange(this.props.checkboxStatus[i])
this.saveAccount(e, i, item.accountNumber, item.accountName)
}}
First toggleChange:
toggleChange = (checkbox) => {
let toggleCheckbox = [...this.state.checkboxes];
toggleCheckbox[checkbox.id].checked = !checkbox.checked
this.setState({
checkboxes: toggleCheckbox
})
this.props.toggleCheckbox(this.state.checkboxes)
}
You're updating the checkboxes property of the state (via this.setState) - all good there. But on the last line, you're passing this.state.checkboxes out. Since this.setState is async, this will likely not reflect the changes you just made (you could send toggleCheckbox instead).
The next function called in the event handler is saveAccount, which contains (partially):
const addAccountState = this.state
if(this.props.checkboxStatus[i].checked === true){
addAccountState.accounts = addAccountState.accounts.concat(data)
this.setState(addAccountState)
this.props.addAccount(addAccountState.accounts)
}
Here you're taking the current value of this.state (which may be old due to the async setState). You update the .accounts property of it, then send the whole thing (which includes .accounts and .checkboxes) to this.setState.
Since the .checkboxes state may have been old (the previous this.setState may not have fired yet), this would queue up the old .checkboxes state to overwrite the new state you tried to save in toggleChange().
A quick and dirty fix there could be to use this.setState({accounts: addAccountState.accounts}) instead, but there may be other issues floating around too (like the modifying of this.state properties directly).
Because setState is asynchronous, subsequent calls in the same update
cycle will overwrite previous updates, and the previous changes will
be lost.
Beware: React setState is asynchronous!
Regarding the separation of store and state... one option might be to not store the checkboxes separately at all, but rather compute them based on which accounts are selected.
It will depend on the needs of your application of course, so I'll be making a few assumptions for the sake of example...
Your application needs a list of selected accounts
Your component needs to show a list of all accounts
Your component has checkboxes for each account: checked = selected = part of application's 'selected accounts' list.
In this case, I would have the list of selected accounts passed in via props.
Within the component I would have the list of all accounts in the local state (if your 'all accounts' list is passed in via props already, then just use that - no local state needed).
Within the render function of the component, I would compute whether the checkbox should be checked or not based on whether or not the account exists in the 'selected accounts' list. If it exists, it's checked. If not, not checked.
Then when the user clicks to check/uncheck the box, I would dispatch the function to add or remove the account from the 'selected accounts' list, and that's it. The account would be added or removed, which would cause your props to update, which would re-render your component, which would check or uncheck the boxes as appropriate.
That may not jive exactly with your particular application's needs, but I hope it gives you some ideas! :)