I want to render an array of html elements in my component. The reason for storing the data/html in an array is because I want to be able to dynamically load a new element depending on a button-click.
This is how I want to display my array:
<div>
{this.state.steps}
</div>
This is how I initiate my component and array:
componentDidMount() {
this.createProcessStep().then(step => {
this.setState({steps: this.state.steps.concat(step)});
});
}
export function createProcessStep() {
this.setState({processStepCounter: this.state.processStepCounter += 1});
return this.addStepToArray().then(d => {
return this.reallyCreateProcessStep()
});
}
addStepToArray = () => {
const step = {
...Some variables...
};
return new Promise(resolve => {
this.setState({
stepsData: this.state.stepsData.concat(step)
}, resolve)
});
};
"stepsData" is another array that holds data (variables) belonging to each step. "steps" on the other hand, should only hold the html.
This is how one step/element looks like:
<div>
...Some Content...
<button label="+" onClick={ () => {
this.createProcessStep().then(step => {
this.setState({
steps: this.state.steps.concat(step)
});
})
}}/>
...other content...
</div>
This button within each step is responsible for loading/adding yet another step to the array, which actually works. My component displays each step properly, however react doesn't properly render changes to the element/step, which is
to say that, whenever e.g. I change a value of an input field, react doesn't render those changes. So I can actually click on the "+"-button that will render the new html element but whenever a change to this element occurs,
react simply ignores the phenotype of said change. Keeping in mind that the changeHandlers for those steps/elements still work. I can change inputfields, radioButtons, checkboxes etc. which will do exactly what it's
supposed to, however the "re-rendering" (or whatever it is) doesn't work.
Any ideas of what I'm doing wrong here? Thanks!
While you could certainly beat your approach into working, I would advise that you take more common react approach.
You make your components to correctly display themselves from the state . ie as many steps are in the state, your component will display. Than make your add button add necessary information (information, not formated html) to the state.
Here is an example how to use component N times:
const MyRepeatedlyOccuringComponent = (n) => (<p key={n}>There goes Camel {n}</p>)
const App = () => {
const camels = [1,22,333,4444,55555]
const caravan = camels.map((n) => MyRepeatedlyOccuringComponent(n))
return(<div>{caravan}</div>
}
Related
I am new to React and I am trying to build a hangman game.
At the moment I am using a hardcoded list of words that the program can choose from. So far everything worked great, but now I am trying to reset the game and the react component that should rerender upon one click only re-renders after two clicks on the reset button and I don't know why
these are the states that I am using :
function App() {
const [numberInList, setNumberInList] = useState(0)
const randomWordsList = ["comfort", "calm", "relax", "coffee", "cozy"];
const [generatedWord, setGeneratedWord] = useState(
randomWordsList[numberInList]
);
const [generatedWordLetters, setGeneratedWordLetters] = useState(
randomWordsList[numberInList].split("").map((letter) => {
return { letter: letter.toUpperCase(), matched: false };
})
);
function resetGame(){
setNumberInList(prev => prev + 1)
setGeneratedWord(randomWordsList[numberInList])
setGeneratedWordLetters(
generatedWord.split("").map((letter) => {
return { letter: letter.toUpperCase(), matched: false };
})
);
setFalseTries(0)
}
this is the reset function I am using
within teh function every state gets updated correctly apart from the generatedWordLetters state, which only gets updated upon clicking the reset button two times.
I can't seem to solve this problem on my own, so any help is appreciated!
Please check useEffect on React. You can use boolean flag as state, put the useEffect parameters like below
React.useEffect(() => {
// here your code works
},[flag])
flag is your boolean state when it changes on reset function, your component re render
Hi i am new ish to JavaScript/React and I am currently making a project to practice it more.
I have an expenses list with some expenses all with a unique Id stored as props.items but i'm trying to add a delete button so that an expense will be removed from props.items when its clicked. Is there a way i can remove an item from props.items with the use of the unique ID?
Currently I have this where idNumber is the unique id sent back from the child component ExpenseItem
const onRemoveExpense = (idNumber) => {
console.log("remove clicked", idNumber)
console.log(props.items, "<- all items")
}
return (
<ul className='expenses-list'>
{props.items.map((expense) => (
<ExpenseItem
key={expense.id}
value={expense.id}
title={expense.title}
amount={expense.amount}
date={expense.date}
removeExpense={onRemoveExpense}
/>
))}
</ul>
);
}
Thanks for the help!
The biggest hurdle I see here is that your items array is not in the state of the component in question-- it is passed in as props. So you'd want to define your deletion script in which component is holding the items in its component state. You'd write it somewhere along the lines of:
const onRemoveExpense = (idNumber) => {
this.setState((state, props) => {
// get items from current state
const { items } = state;
// get a new array with only those items that do *not* have the id number passed
const newItems = items.filter((item) => item.id !== idNumber);
// return it to set the new state
return newItems;
});
}
This would obviously need to be adjusted to your specific state and component structure. You'd then pass this as a prop along with the items to the component in question, and call it to trigger a deletion.
For a "hide" function instead of a delete one, you could try adding a shown boolean prop and then change that on click.
But to actually delete it, you'll need to have your items stored in state.
You could try something like this:
const [items, setItems] = useState(props.items)
// set the initial state as `props.items`
// (I'm assuming the code snippet you shared exists inside a functional component)
const onRemoveExpense = (idNumber) => {
console.log("remove clicked", idNumber)
console.log(props.items, "<- all items")
const newItems = items.filter(({ id }) => id !== idToDelete)
setItems(newItems)
}
return (
<ul className='expenses-list'>
{items.map((expense) => (
<ExpenseItem
key={expense.id}
value={expense.id}
title={expense.title}
amount={expense.amount}
date={expense.date}
removeExpense={() => onRemoveExpense(expense.id)}
/>
))}
</ul>
);
}
I might be forgetting something though—I haven't tested the above code. You might need to have a useEffect() to make it re-render properly when the state changes.
Or you can manage the state in the component that is defining items for this component.
I'm making a chess game using React and I'm trying to write a function that computes the squares that should be highlighted when a certain chess piece is selected. I have the game as one component and each chess piece as a second component, and the button in the chess component calls a function from the game component when clicked.
I want to highlight the appropriate squares if and only if the selected piece is the correct color, so I had a setState call in an if statement in my handleClick() function. After reading Issue with setState() in if statement I moved the conditional so that the handleClick function is only linked to pieces of the correct color.
The issue now is that the state gets changed as desired, but for some reason the components don't rerender to reflect that change. Can someone please let me know how I could fix this? Here's my code for handling clicks:
handleClick(num){
this.setState(prevState => {
return {
gameBoard: prevState.gameBoard,
turn: prevState.turn,
selected: num
}
})
}
and here's my code for creating the board:
<div>
{
this.state.gameBoard.map((object)=>
<div className = "board-row"> {object.map((object2) =>
<Piece key={object2.at} turn = {this.state.turn} selected = {object2.selected} piece = {object2.piece} identifier = {object2.at} onClick = {() => this.handleClick(object2.at)} color = {(object2.at+Math.floor(object2.at/8))%2 === 0?"white":"black"} />)}
</div>
)
}
</div>
try:
handleClick(num){
this.setState(prevState => {
return {
...prevState,
selected: num
};
})
}
Ths issue here is mostly a question of dependencies. Your rendering is based on the gameboard field of the state, which isn't being modified. The component therefore doesn't rerender since it doesn't know that there are additional dependencies (selected) for it.
The easiest and cleanest approach I would suggest is to simply move your map function inside a useCallback (or useMemo, both should work) with the proper dependencies, then simply use it in your rendering method.
const foobar = useCallback( () => this.state.gameBoard.map((object)=>
<div className = "board-row"> {object.map((object2) =>
<Piece key={object2.at} turn = {this.state.turn} selected = {object2.selected} piece = {object2.piece} identifier = {object2.at} onClick = {() => this.handleClick(object2.at)} color = {(object2.at+Math.floor(object2.at/8))%2 === 0?"white":"black"} />)}
</div>
)
, [this.state] )
render (
<div>
{foobar()}
</div>
)
I have a functional component that keeps a state. This state I try to manipulate using an onClick event in an SVG. The SVG is in another component and has the addAndRemoveSelectedCabin method passed to it via props. I loop through the elements in an useEffect and add an eventListener. This doesn't work. The useEffect with the selectedCabins dependency logs the new number only. It seems the state returns to the initial state after every stateChange.
This is the state and method in the parent component.
const [selectedCabins, setSelectedCabins] = useState([0]);
const addRemoveSelectedCabin = id => {
const newArr = [...selectedCabins, id];
setSelectedCabins(newArr);
}
useEffect(() => {
console.log(selectedCabins);
}, [selectedCabins])
This is how I call the method. [UPDATE]
useEffect(() =>
{
const cabins = document.querySelectorAll(".cabin");
cabins.forEach(cabin =>
{
const id = cabin.getAttributeNS(null, "id").substring(1, 5);
const found = cabinsData.find(el => el.id === id)
if (found && found.status === "available")
{
cabin.classList.add("green")
cabin.addEventListener('click', () => addRemoveSelectedCabin(id));
} else if (found && found.status === "booked")
{
cabin.classList.add("gray")
}
})
}, [])
Console:
[0]
(2) [0, "1105"]
(2) [0, "1101"]
This works if I put the onClick directly in the SVG element. Does anyone know why this is?
<rect
id="C1105"
x="749.4"
y="58.3"
className="cabin"
width="36.4"
height="19.9"
onClick={() => addRemoveSelectedCabin(1105)}
>
<title>1105</title>
</rect>
As I said in my comment, you are binding addRemoveSelectedCabin in the first render. useEffect is only executed once since you pass an empty dependency list. addRemoveSelectedCabin closes over selectedCabins which at that point in time has the value [0].
Why am I seeing stale props or state inside my function? from the React documentation has more information about this.
The solution in your case is simple: Pass a function to the setter to get the "current" state value. Don't reference the state value in the component:
const addRemoveSelectedCabin = id => {
setSelectedCabins(selectedCabins => [...selectedCabins, id]);
}
Having said that, this is still an odd thing to do in React world. You should reevaluate your assumptions that make you think you have to do it that way.
It's not all the elements that should have a click listener.
Depending on how you actually render the elements, that's easy to do. JSX/React is just JavaScript. Whether you have a condition that adds the event handler or not or whether you have a condition that sets onClick or not is basically the same.
But without a more complete example there is not much we can suggest.
I have a ReactJS app where I am
A) reading a JSON input that describes a form's structure
B) dynamically generating a form from this JSON input (using document.createElement(..))
The JSON would look something like this:
{
formElements: [
{
id: “dd1”,
type: “dropdown”,
options: [ {value: “first”}, {value: “second”}]
},
{
id: “tf1”,
type: “textfield”,
showIf: “dd1 == ‘second’”
}
]
}
Now the tricky thing is that the JSON input file not only describes which form elements (e.g. dropdown, radio button group, text field) etc should be present but it ALSO describes show/hide logic for each element. For example, if a particular dropdown selection is made, then a textfield should be shown (otherwise it should stay hidden).
This would normally be done in jQuery but I have heard jQuery is not a good idea with React.
If these were hardcoded form elements, I could easily code this show/hide logic. The problem is that the form elements are being dynamically generated (by reading that JSON file) and I need to apply this show/hide logic on the fly to these autogenerated form elements.
I'm not sure how to do this.
If any one has suggestions for approaches here, especially with examples, that would be much appreciated. Thank you!
You should still be able to apply conditional rendering logic to the JSX code that is generating your form, but have you looked into using an existing form library like react-form or redux-forms? If you're relatively new to react, this would be a much easier route to get the results you want. I can't recommend a particular form library, but react-form notes that it handles dynamic data.
Here is my rough sketch of how you could manage this without using redux or a built-in form library. This is a draft that was imagined but never executed, so treat it like psuedo-code and definitely not optimized:
//import base form components up here (input, checkbox, etc)
// Map the strings in your field object to the component imported or defined above
const fieldSelector = {
input : Input,
textarea: TextArea,
checkbox: CheckBox
}
Class CustomForm extends React.Component {
constructor(props) {
super(props);
const fields = {}
const byId = []
// Note if there is any asynchronous data, you might want to put this logic
// in componentDidMount
// Create an array with each 'id'
const byId = this.props.formData.map( item => item.id );
// Create a map object listing each field by its id
this.props.formData.forEach( (field) => {
fields[field.id] = field;
}
this.state = {
fields,
byId,
}
this.handleChange = this.handleChange.bind(this);
this.checkVisibility = this.checkVisibility.bind(this);
}
// Need to add some additional logic if you're using checkboxes
// Creates an event handler for each type of field
handleChange(id) {
return (event) => {
const updatedFields = {...this.state.fields};
updatedFields[id].value = event.target.value
this.state.byId.forEach( fieldId => {
updatedFields[fieldId].visible = checkVisibility(updatedFields, fieldId)};
}
this.setState({fields: updatedFields})
}
}
// You can either restructure your showIf or include a function above
// to parse our the elements of the expression.
checkVisibility(updatedFields, fieldId) {
const field = updatedFields[fieldId];
const showIfId = field.showIf.triggerFieldId;
const showIfValue = field.showIf.value;
const operator = field.showIf.operator;
switch(operator){
case '===':
return updatedFields[showIfId].value === ShowIfValue;
case '<':
return updatedFields[showIfId].value < ShowIfValue;
//...fill in rest of operators here
default:
return field.visible;
}
}
render() {
return this.state.byId.map( fieldId => {
const field = this.state.fields[fieldId];
const CustomField = FieldSelector[field.type]
return (
{field.visible &&
<CustomField {insert whatever props from field} />
}
);
});
}
}
export default CustomForm