I am using React and found something which was not quite working, but it made sense to me totally:
const [Res, setRes] = useState(<div></div>);
const test = (e) => {
if (e.keyCode === 13) {
setRes( prevState => prevState.append(<p>{e.target.value)}</p>))
}
}
return(
{Res}
)
If this us wrong, please tell me the correct way to solve similar problems please.
Keeping JSX in state is an antipattern.
Instead of keeping the JSX in an array in state, keep the data in the array without JSX, then build the JSX when you are ready to render:
const YourComponent = () => {
const [res, setRes] = useState([]);
const test = (e) => {
const {value} = e.target;
if (e.code === "Enter") {
setRes(prev => [...prev, value]);
}
};
return (
<div>
{res.map(e => <p>{e}</p>)}
</div>
);
};
<p> is using index as key, which is a problem. How to fix it is application-specific depending on where your data is coming from and whether it can be removed from the array, whether it's unique, etc, but ideally generate an id.
Also, e.target.value shouldn't be accessed in a state setter callback. It's async so the event object might have gone stale by the time it's read. Pull out the primitive value into the handler closure.
I suggest picking better names: test and res are pretty meaningless.
Finally, instead of e.keyCode === 13, use e.code === "Enter".
Related
Im trying to set a counter everytime this condition is met in every array.push :
interface FilterProps {
filterQuery: any
setFilterQuery: (e: any) => any
resetFilter: (e: any) => any
handleCategory: (e: any) => any
categoryList: any
getCategoryValue: any
handleOnClick: (e: any) => any
paginateOnClick: (e: any) => any
paginateIcon: any
handleToggle: (e: any) => any
checkState: any
expandFilter: boolean
printEvent: PrintEvent
}
export const EventFilter: React.FC<FilterProps> = ({
filterQuery,
setFilterQuery,
resetFilter,
handleCategory,
categoryList,
getCategoryValue,
handleOnClick,
paginateOnClick,
paginateIcon,
handleToggle,
checkState,
expandFilter,
}, printEvent: PrintEvent) => {
const [countUnlabeled, setCountUnlabeled] = React.useState(0)
const classes = useStyles()
const { box, ui } = useStores()
const { labels } = printEvent
let unlabeledEvents: any[] = []
function getUnlabeled() {
box.descEvents.forEach((printEvent: PrintEvent) => {
const isStopEvent =
(printEvent && printEvent.name === 'control_fault') ||
(printEvent.name === 'running' && printEvent.value === false) ||
(printEvent.name === 'safety_chain' && printEvent.value === false) ||
(printEvent.name === 'torch_collision' && printEvent.value === true) ||
(printEvent.name === 'motion_sup' && printEvent.value === true) ||
(printEvent.name === 'e_stop' && printEvent.value === true)
const unlabeled = printEvent.labels === null && isStopEvent
if (unlabeled) {
unlabeledEvents.push(unlabeled)
ui.setUnlabeledCount(unlabeledEvents.length)
}
})
}
useEffect(() => {
if (box.descEvents && printEvent) {
getUnlabeled()
console.log('useEffect just ran', ui.unlabeledCount, unlabeledEvents.length)
}
}, [unlabeledEvents, ui.unlabeledCount, printEvent.name])
return (
<Accordion
className={classes.eventAccordion}
TransitionProps={{ unmountOnExit: true }}
defaultExpanded={expandFilter}
>
<AccordionSummary>
<div className={classes.filterHeader}>
<div className={classes.filterText}>
<FilterListIcon />
<p>Filter by:</p>
</div>
<div className={classes.unfiltered}>
Unlabeled events:
<Chip
size="small"
label={ui.unlabeledCount}
className={classes.chipMissing}
/>
</div>
</div>
</AccordionSummary>
</Accordion>
export default EventFilter
normally it should run the functuion check everytime the event is pushed or there are changes in the array, but its not counting sychronously.
i tried adding a count to the unlabeled conditional but doesnt work and dont want to overcomplicate things here.
What is the problem here?
counter example
In React, stuff you write inside the functional component body is run on every render. That means
let unlabeledEvents: any[] = [];
useEffect(() => {
console.log(unlabeledEvents);
}, [unlabeledEvents]);
creates a fresh array every time the component is rendered, leaving the object reference (or length, or any other property) unchanged. If you want to react changes to an object, you need a way to store the old reference somewhere and then update it in a way that creates a new reference. This is exactly what the useState hook is for. Just make sure a new array is created every time in order to update that reference:
const [unlabeledEvents, setUnlabeledEvents] = useState<any[]>([]);
function getUnlabeled() {
// find unlabeled item
if (unlabeled) {
setUnlabeledEvents([...unlabeledEvents, unlabeled]);
}
}
This way your useEffect runs every time getUnlabeled adds a new entry to the unlabeledEvents array. You can also ditch countUnlabeled, since unlabeledEvents will always be up to date, you can use its length directly instead.
However, are you sure your useStores hooks works as expected? Since we don't know anything about it, it could suffer from the same problem as described above. And if it does, I'd recommend using useMemo instead, to recalculate the array every time box is changed (since you're iterating the entire array anyways):
const { box } = useStores();
const unlabeledEvents = useMemo(
box.descEvents.filter((e) => true /* your unlabeled condition here */),
[box]
)
Also check out this question for some more details on updating stateful arrays in a component.
I have an array state for some checkboxes where I am catching the labels for those that are true (checked). Must ignore the false.
I am able to generate a list of checked checkboxes thanks to some of you in another thread. But I'm hitting another wall with the select all toggle.
const handleSwitch = (e) => {
if(e.target.checked) {
setActive(true);
const updatedCheckedState = checkedState.map(element => element.checked = true);
setCheckedState([...updatedCheckedState]);
} else {
setActive(false)
const updatedCheckedState = checkedState.map(element => element.checked = false);
setCheckedState([...updatedCheckedState]);
}
}
This function above in particular. Likewise, if I manually check all of the checkboxes inside, it needs to know that all are selescted and make the active state = true. If I can get help with at least the first part, I'm confident I can solve the other part myself.
Here's a sandbox if you want to mess around with it. Thanks
Your sandbox is quite broken. The way you are tracking checked state is internally inconsistent.)
The main culprits (in Filter.js) are:
on line 119, you treat checkedState like a dictionary, but in handleSwitch and handleOnChange you treat it like an array (but the logic inside is still non-functional for the array approach as far as I can tell.
if you want it to be an array, let it be a string-valued "checkedLabels" array, and set checked on your checkbox component to checkedLabels.includes(item.label)
if you want it to be a dictionary:
handleOnChange needs to simply toggle the currently clicked element, like so [e.target.name]: !checkedState[e.target.name]
handleSwitch needs to add an entry for every element in data, set to true or false as appropriate.
Example (codesandbox):
const handleSwitch = (e) => {
if (e.target.checked) {
setActive(true);
setCheckedState(
Object.fromEntries(data.map((item) => [item.label.toLowerCase(), true]))
);
} else {
setActive(false);
setCheckedState({});
}
};
const handleOnChange = (e) => {
setCheckedState({
...checkedState,
[e.target.name]: !checkedState[e.target.name]
});
};
<CustomCheckbox
size="small"
name={item.label.toLowerCase()}
checked={checkedState[item.label.toLowerCase()] ?? false}
onChange={handleOnChange}
/>
EDIT from OP
I tweaked the hnadleOnChange function to
const handleOnChange = (e) => {
if (e.target.checked) {
setCheckedState({
...checkedState,
[e.target.name]: !checkedState[e.target.name]
});
} else {
const updatedCheckedState = {...checkedState};
delete updatedCheckedState[e.target.name];
setCheckedState(updatedCheckedState);
}
};
Before, it allowed for false values to be added when you unchecked a previously checked checkbox. This removes it
Edit: To do this with an array, you'll want to add to the array when checking, and remove from it when un-checking. Then do an includes to see if an individual checkbox should be checked.
Also, you can do a simple setActive(newCheckedItemLabels.length === data.length); in the handleOnChange to achieve your other requirement.
This codesandbox does everything you need with arrays instead of objects.
Notably:
const [checkedItemLabels, setCheckedItemLabels] = useState([]);
const handleSwitch = (e) => {
if (e.target.checked) {
setActive(true);
setCheckedItemLabels(data.map((item) => item.label.toLowerCase()));
} else {
setActive(false);
setCheckedItemLabels([]);
}
};
const handleOnChange = (e) => {
const newCheckedItemLabels = checkedItemLabels.includes(e.target.name)
? checkedItemLabels.filter((label) => label !== e.target.name)
: [...checkedItemLabels, e.target.name];
setCheckedItemLabels(newCheckedItemLabels);
setActive(newCheckedItemLabels.length === data.length);
};
<CustomCheckbox
size="small"
name={item.label.toLowerCase()}
checked={checkedItemLabels.includes(
item.label.toLowerCase()
)}
onChange={handleOnChange}
/>
Adding to #JohnPaulR answer. You can add useEffect hoot to achieve additional requirements you have.
if I manually check all of the checkboxes inside, it needs to know that all are selected and make the active state = true.
useEffect(() => {
const checkedTotal = Object.keys(checkedState).reduce((count, key) => {
if (checkedState[key]) {
return count + 1;
}
}, 0);
setActive(data.length === checkedTotal);
}, [checkedState]);
A full working example https://codesandbox.io/s/still-water-btyoly
I've got something I don't understand here. I'm creating a hierarchy of React functional components like so:
const ContainerComponent = () => {
const [total, setTotal] = useState(initialValue);
const updateFunction = () => { console.log('It got called'); }
return (
<EntryComponent updateFunction={updateFunction} />
);
}
const EntryComponent = ({updateFunction}) => {
const services = [{}, {}, {}];
return (
{services.map((service) => <ServiceComponent updateFunction={updateFunction} />}
);
}
const ServiceComponent = ({updateFunction}) => {
return (
<input type='checkbox' onChange={updateFunction} ></input>
);
}
So the idea is that the function gets passed all the way to the ServiceComponent and attached to the checkbox for each component. On change the function should fire, running the code and updating the total value in the ContainerComponent. Problem is that this isn't happening properly and the function seems only to be passed to the first ServiceComponent instance. If I drop a console.log(updateFunction) into ServiceComponent I see the function logged for the first component, and undefined for the remaining two. This is strange even for JavaScript. Can anyone shed any light as to what is going on? As far as I understand the function should be able to be passed like any other variable, and each ServiceComponent should have it to use when needed. Indeed, if I pass a second property to the child, an object or a primitive like an integer it comes through just fine on every child component. Why is this not occurring for my function, and how do I fix it?
Edit: I see the problem, and the problem is I'm not as smart as I think I am. In the condensed version I put here everything is being supplied to the child components in the same loop, but in the actual project the child components are created in multiple places and I neglected to pass the function property to all of them. Which is mind mindbogglingly stupid on my part. I'm leaving the question here as many of you have posted replies, for which I'm grateful.
Programming is hard, I think I need a better brain.
I tried refactoring your code to this
const ContainerComponent = () => {
const [total, setTotal] = useState(0);
const updateFunction = (e) => {
console.log("update total stuff happens here");
console.log(e);
};
return <EntryComponent updateFunction={updateFunction} />;
};
const EntryComponent = ({ updateFunction }) => {
const services = ["one", "two", "three"];
return (
<>
{services.map((service) => (
<ServiceComponent updateFunction={updateFunction} />
))}
</>
);
};
const ServiceComponent = ({ updateFunction }) => (
<input type="checkbox" onChange={updateFunction}></input>
);
and it works just fine.
Try also using a react fragment in your entry component.
I've created two components which together create a 'progressive' style input form. The reason I've chosen this method is because the questions could change text or change order and so are being pulled into the component from an array stored in a JS file called CustomerFeedback.
So far I've been trying to add a data handler function which will be triggered when the user clicks on the 'Proceed' button. The function should collect all of the answers from all of the rendered questions and store them in an array called RawInputData. I've managed to get this to work in a hard coded version of SurveyForm using the code shown below but I've not found a way to make it dynamic enough to use alongside a SurveyQuestion component. Can anybody help me make the dataHander function collect data dynamically?
There what I have done:
https://codesandbox.io/s/angry-dew-37szi2?file=/src/InputForm.js:262-271
So, we can make it easier, you just can pass necessary data when call handler from props:
const inputRef = React.useRef();
const handleNext = () => {
props.clickHandler(props.reference, inputRef.current.value);
};
And merge it at InputForm component:
const [inputData, setInputData] = useState({});
const handler = (thisIndex) => (key, value) => {
if (thisIndex === currentIndex) {
setCurrentIndex(currentIndex + 1);
setInputData((prev) => ({
...prev,
[key]: value
}));
}
};
// ...
<Question
// ...
clickHandler={handler(question.index)}
/>
So, you wanted array (object more coninient I think), you can just save data like array if you want:
setInputData(prev => [...prev, value])
Initially, I thought you want to collect data on button clicks in the InputForm, but apparently you can do without this, this solution is simpler
UPD
Apouach which use useImperativeHandle:
If we want to trigger some logic from our child components we should create handle for this with help of forwarfRef+useImperativeHandle:
const Question = React.forwardRef((props, ref) => {
const inputRef = React.useRef();
React.useImperativeHandle(
ref,
{
getData: () => ({
key: props.reference,
value: inputRef.current.value
})
},
[]
);
After this we can save all of our ref in parent component:
const questionRefs = React.useRef(
Array.from({ length: QuestionsText.length })
);
// ...
<Question
key={question.id}
ref={(ref) => (questionRefs.current[i] = ref)}
And we can process this data when we want:
const handleComplete = () => {
setInputData(
questionRefs.current.reduce((acc, ref) => {
const { key, value } = ref.getData();
return {
...acc,
[key]: value
};
}, {})
);
};
See how ref uses here:
https://reactjs.org/docs/forwarding-refs.html
https://reactjs.org/docs/hooks-reference.html#useimperativehandle
I still strongly recommend use react-hook-form with nested forms for handle it
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.