I'm having a really weird problem with my React Native component. Component's state is correctly updated, but it is not reflected on what is rendered by the component.
Here is what my component looks like (I didn't include imports and other parts of the code that are irrelevant):
export default function MessageActionsModal({
message,
actions,
onClose
}) {
const [dimensions, setDimensions] = useState(null);
const [messageActions, setMessageActions] = useState([]);
useEffect(() => {
if (message !== null) {
setMessageActions(actions.map((a) => ({
...a,
onPress: () => {
a.onPress(message?.item);
onClose();
}
})))
} else {
setMessageActions([]);
}
}, [message, actions, onClose]);
if (message === null) {
return null;
}
return (
<Portal>
<TouchableOpacity
style={styles.messageActionsModal}
activeOpacity={0}
onPress={onClose}
>
<Portal style={styles.messageActionsModalInnerContainer}>
<MessageListItem
style={[
styles.message,
getMessagePositionAndDimensions(message)
]}
{...message.item}
/>
<View
style={[
styles.messageActionsMenu,
getMenuPosition(dimensions, message)
]}
{...dimensions === null && {
onLayout: ({ nativeEvent }) => setDimensions({
width: nativeEvent.layout.width,
height: nativeEvent.layout.height
})
}}
>
<Text>{'LEN'}{messageActions.length}</Text>
<Text>{'LOG'}{console.log(messageActions.length)}</Text>
{messageActions.map((a) => (
<Button
key={a.title}
style={styles.messageActionButton}
title={a.title}
variant="text"
startIcon={a.icon}
startIconProps={{
style: styles.messageActionButtonIconWrapper,
iconStyle: styles.messageActionButtonIcon,
fill: Colors.yellow
}}
onPress={a.onPress}
/>
))}
</View>
</Portal>
</TouchableOpacity>
</Portal>
);
}
So basically, after messageActions are updated in the useEffect, component renders incorrectly and neither message action is rendered (although one item should be rendered).
Another interesting thing is that the following code renders "LEN 0", but it logs "LOG 1" in the console.
<Text>{'LEN'}{messageActions.length}</Text>
<Text>{'LOG'}{console.log(messageActions.length)}</Text>
What am I doing wrong here?
You can try memoizing actions instead of using useEffect. Here's an example:
const messageActions = useMemo(() => {
if (message === null) {
return [];
}
return actions.map((a) => ({
...a,
onPress: () => {
a.onPress(message?.item);
onClose();
}
}));
}, [message, actions, onClose]);
Related
I've a weird behavior here.
I'm trying to update a parent component from a child.
I've thus something like this for the child:
const LabelList = ({editable, boardLabels, cardLabels, size='normal', udpateCardLabelsHandler}) => {
return (
<DropDownPicker
labelStyle={{
fontWeight: "bold"
}}
badgeColors={badgeColors}
showBadgeDot={false}
items={items}
multiple={true}
open={open}
onChangeValue={(value) => udpateCardLabelsHandler(value)}
value={value}
setOpen={setOpen}
setValue={setValue} />
)
}
And, for the parent, something like this:
const CardDetails = () => {
const [updatedCardLabels, setUpdatedCardLabels] = useState([])
const [card, setCard] = useState({})
const [editMode, setEditMode] = useState(false)
// Handler to let the LabelList child update the card's labels
const udpateCardLabelsHandler = (values) => {
const boardLabels = boards.value[route.params.boardId].labels
const labels = boardLabels.filter(label => {
return values.indexOf(label.id) !== -1
})
console.log('updated labels',labels)
setUpdatedCardLabels(labels)
}
return (
<View style={{zIndex: 10000}}>
<Text h1 h1Style={theme.title}>
{i18n.t('labels')}
</Text>
<LabelList
editable = {editMode}
boardLabels = {boards.value[route.params.boardId].labels}
cardLabels = {card.labels}
udpateCardLabelsHandler = {udpateCardLabelsHandler} />
</View>
)
And, this just doesn't work: As soon as I try changing something in the DropDownPicker the application hangs. The console.log statement isn't even executed and no errors show up in my expo console.
What's strange is that if I change the updateCardLabels state to be a boolean for example, everything works ok (eg: the console.log statement is executed):
const [updatedCardLabels, setUpdatedCardLabels] = useState(false)
// Handler to let the LabelList child update the card's labels
const udpateCardLabelsHandler = (values) => {
const boardLabels = boards.value[route.params.boardId].labels
const labels = boardLabels.filter(label => {
return values.indexOf(label.id) !== -1
})
console.log('updated labels',labels)
setUpdatedCardLabels(true)
}
Please note that updatedCardLabels isn't used anywhere: it's a dummy variable that I'm just using to debug this issue (to make sure I was not ending in some endless render loop or something similar).
For the sake of completeness, here's what labels looks like at line console.log('updated labels',labels) (please not that I can only see this value when doing setUpdatedCardLabels(true) as otherwise, when the code does setUpdatedCardLabels(labels), the console.log statement is not executed, as mentioned earlier):
updated labels Array [
Object {
"ETag": "a95b2566521a73c5edfb7b8f215948bf",
"boardId": 1,
"cardId": null,
"color": "CC317C",
"id": 9,
"lastModified": 1621108392,
"title": "test-label",
},
]
Does anybody have an explanation for this strange behavior?
Best regards,
Cyrille
So, I've found the problem: It was a side effect of the DrowpDownPicker.
I've solved it by changing my child as follow:
const LabelList = ({editable, boardLabels, cardLabels, size='normal', udpateCardLabelsHandler}) => {
const [open, setOpen] = useState(false);
const [value, setValue] = useState(cardLabels.map(item => item.id));
const theme = useSelector(state => state.theme)
// Updates parent when value changes
useEffect(() => {
if (typeof udpateCardLabelsHandler !== 'undefined') {
udpateCardLabelsHandler(value)
}
}, [value])
return (
<DropDownPicker
labelStyle={{
fontWeight: "bold"
}}
badgeColors={badgeColors}
showBadgeDot={false}
items={items}
multiple={true}
open={open}
value={value}
setOpen={setOpen}
setValue={setValue} />
)
I have a functional component that is a modal. Inside I am doing a map on a array to render picture. I would like to add a div on the picture of one picture when clicking on it. However, it appears that the UI is not updated inside of the map even when using useState. Any idea on how to solve the issue?
Here is the code (I removed Style and things that were not important):
const CreateBoardModal = ({ closeModal, isModalOpen, searchResults, ...props }) => {
Modal.setAppElement('body')
const [movies, setMovies] = useState([])
const [searchResultsAdded, setSearchResultAdded] = useState({})
const addMovie = (movieId) => {
if (movies.includes(movieId)) {
console.log("Already in the list")
} else {
console.log("Added to the list!")
var movies_temp = movies
movies_temp.push(movieId)
setMovies(movies_temp)
}
var searchMoviesTemp = searchResultsAdded
searchMoviesTemp[movieId] = true
setSearchResultAdded(searchMoviesTemp)
console.log(searchResultsAdded) //Here everything is updated as expected
}
return (
<Modal
isOpen={isModalOpen}
onRequestClose={closeModal}
>
<div>
{searchResults.length > 0 &&
searchResults.map((movie, index) => {
return (
<div style={{ margin: 10 }} onClick={() => addMovie(movie.id)}>
<Movie
title={movie.original_title}
voteAverage={movie.vote_average}
posterPath={movie.poster_path}
></Movie>
{searchResultsAdded[movie.id] &&
<div>Added ✓</div> } {/*This is shown only when I hot reload the react app*/}
</div>
)
})}
</div>
</Modal >
)
}
export default CreateBoardModal;
I have two React components, namely, Form and SimpleCheckbox.
SimpleCheckbox uses some of the Material UI components but I believe they are irrelevant to my question.
In the Form, useEffect calls api.getCategoryNames() which resolves to an array of categories, e.g, ['Information', 'Investigation', 'Transaction', 'Pain'].
My goal is to access checkboxes' states(checked or not) in the parent component(Form). I have taken the approach suggested in this question.(See the verified answer)
Interestingly, when I log the checks it gives(after api call resolves):
{Pain: false}
What I expect is:
{
Information: false,
Investigation: false,
Transaction: false,
Pain: false,
}
Further More, checks state updates correctly when I click into checkboxes. For example, let's say I have checked Information and Investigation boxes, check becomes the following:
{
Pain: false,
Information: true,
Investigation: true,
}
Here is the components:
const Form = () => {
const [checks, setChecks] = useState({});
const [categories, setCategories] = useState([]);
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
}
useEffect(() => {
api
.getCategoryNames()
.then((_categories) => {
setCategories(_categories);
})
.catch((error) => {
console.log(error);
});
}, []);
return (
{categories.map(category => {
<SimpleCheckbox
label={category}
onCheck={handleCheckChange}
key={category}
id={category}
/>
}
)
}
const SimpleCheckbox = ({ onCheck, label, id }) => {
const [check, setCheck] = useState(false);
const handleChange = (event) => {
setCheck(event.target.checked);
};
useEffect(() => {
onCheck(check, id);
}, [check]);
return (
<FormControl>
<FormControlLabel
control={
<Checkbox checked={check} onChange={handleChange} color="primary" />
}
label={label}
/>
</FormControl>
);
}
What I was missing was using functional updates in setChecks. Hooks API Reference says that: If the new state is computed using the previous state, you can pass a function to setState.
So after changing:
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
}
to
const handleCheckChange = (isChecked, category) => {
setChecks(prevChecks => { ...prevChecks, [category]: isChecked });
}
It has started to work as I expected.
It looks like you're controlling state twice, at the form level and at the checkbox component level.
I eliminated one of those states and change handlers. In addition, I set checks to have an initialState so that you don't get an uncontrolled to controlled input warning
import React, { useState, useEffect } from "react";
import { FormControl, FormControlLabel, Checkbox } from "#material-ui/core";
import "./styles.css";
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Form />
</div>
);
}
const Form = () => {
const [checks, setChecks] = useState({
Information: false,
Investigation: false,
Transaction: false,
Pain: false
});
const [categories, setCategories] = useState([]);
console.log("checks", checks);
console.log("categories", categories);
const handleCheckChange = (isChecked, category) => {
setChecks({ ...checks, [category]: isChecked });
};
useEffect(() => {
// api
// .getCategoryNames()
// .then(_categories => {
// setCategories(_categories);
// })
// .catch(error => {
// console.log(error);
// });
setCategories(["Information", "Investigation", "Transaction", "Pain"]);
}, []);
return (
<>
{categories.map(category => (
<SimpleCheckbox
label={category}
onCheck={handleCheckChange}
key={category}
id={category}
check={checks[category]}
/>
))}
</>
);
};
const SimpleCheckbox = ({ onCheck, label, check }) => {
return (
<FormControl>
<FormControlLabel
control={
<Checkbox
checked={check}
onChange={() => onCheck(!check, label)}
color="primary"
/>
}
label={label}
/>
</FormControl>
);
};
If you expect checks to by dynamically served by an api you can write a fetchHandler that awaits the results of the api and updates both slices of state
const fetchChecks = async () => {
let categoriesFromAPI = ["Information", "Investigation", "Transaction", "Pain"] // api result needs await
setCategories(categoriesFromAPI);
let initialChecks = categoriesFromAPI.reduce((acc, cur) => {
acc[cur] = false
return acc
}, {})
setChecks(initialChecks)
}
useEffect(() => {
fetchChecks()
}, []);
I hardcoded the categoriesFromApi variable, make sure you add await in front of your api call statement.
let categoriesFromApi = await axios.get(url)
Lastly, set your initial slice of state to an empty object
const [checks, setChecks] = useState({});
I am using usestate hook in react 16.10.2, but after updating initial state using custom function in usetate hook, react does not trigger a re-render(OtherComponent is not rendered), Here is my react component:
import React, { useState, useEffect } from 'react';
import OtherComponent from "./OtherComponent";
function Component(props) {
const [render, setRender] = useState({0:false, 1:false});
const display_data = (index) => {
setRender((prevState) => {
prevState[index] = !prevState[index];
return prevState;
});
};
return (
<>
{{custom_json_array}.map((record, index) => {
return (
<div>{teacher_render[index] ? 'true' : 'false'}</div>
<button onClick={() => display_data(index)}>change state</button>
{render[index] ? <OtherComponent /> : ''}
</div>)
})}
</>
);
}
But strange thing is if I return {...prevState} from hook updater function, everything is normal and re-render is triggerd!
I am totally confused, why react behaves like this?!
I assume the problem is that you are mutating render?
<button
onClick={() => setRender({ ...render, [index]: !render[index] })}
>
change state
</button>
In this example, click on the names to see the custom component and click again to hide
https://codesandbox.io/s/dark-wind-3310q
const CustomComponent = () => (
<div style={{ marginLeft: 10, background: "red" }}>I'm Selected!</div>
);
function App() {
const [people] = useState([
{ id: 0, name: "Mario" },
{ id: 1, name: "Luigi" },
{ id: 2, name: "Peach" }
]);
const [selected, setSelected] = useState({});
return (
<div>
{people.map(({ id, name }) => (
<div
style={{ display: "flex", cursor: "pointer" }}
key={id}
onClick={() => setSelected({ ...selected, [id]: !selected[id] })}
>
{name}
{selected[id] && <CustomComponent />}
</div>
))}
</div>
);
}
Here is a simplified example of the erroneous code you posted, to show no update occurring:
https://codesandbox.io/s/goofy-williamson-22fgs
function App() {
const [obj, setObj] = useState({ name: "Mario" });
const change = () => {
// The following commented code will display no change
// obj.name = "Peach";
// setObj(obj);
setObj({ ...obj, name: "Peach" });
};
return (
<div className="App">
<div>{obj.name}</div>
<button onClick={change}>Change!</button>
</div>
);
}
Your onclick handler is called display_teacher_data in the markup but display_data in the component. Set them to be the same so the state changes.
I have this child component called TodoList
const TodoItem = ({ checked, children }) =>
(<TouchableOpacity
style={{ backgroundColor: checked && 'red'}}>
{children}
</TouchableOpacity>
);
const TodoList = props => {
const {
options = [],
onSelect,
...rest
} = props;
const [selectedOptionIndex, setSelectedOptionIndex] = useState(null);
useEffect(() => {
onSelect(options[selectedOptionIndex]);
}, [onSelect, options, selectedOptionIndex]);
const renderItem = (o, index) => {
return (
<TodoItem
key={o + index}
onPress={() => setSelectedOptionIndex(index)}
checked={index === selectedOptionIndex}>
{index === selectedOptionIndex && <Tick />}
<Text>{o}</Text>
</TodoItem>
);
};
return (
<View {...rest}>{options.map(renderItem)}</View>
);
};
export default TodoList;
And I have a parent component called Container
export default function() {
const [item, setItem] = setState(null);
return (
<Screen>
<TodoList options={[1,2,3]} onSelect={(i) => setItem(i)} />
</Screen>
)
}
I want to have a callback from child component to parent component using onSelect whenever a TodoItem is selected. However, whenever the onSelect is called, my TodoList re-renders and my selectedOptionIndex is reset. Hence, my checked flag will only change to true briefly before resetting to false.
If I remove the onSelect callback, it works fine. But I need to setState for both child and parent. How do I do that?
It's hard to tell why thats happening for you, most likely because the container's state is changing, causing everything to rerender.
Something like this should help you out, though.
const { render } = ReactDOM;
const { useEffect, useState } = React;
const ToDoItem = ({checked, label, onChange, style}) => {
const handleChange = event => onChange(event);
return (
<div style={style}>
<input type="checkbox" checked={checked} onChange={handleChange}/>
{label}
</div>
);
}
const ToDoList = ({items, onChosen}) => {
const [selected, setSelected] = useState([]);
const handleChange = item => event => {
let s = [...selected];
s.includes(item) ? s.splice(s.indexOf(item), 1) : s.push(item);
setSelected(s);
onChosen(s);
}
return (
<div>
{items && items.map(i => {
let s = selected.includes(i);
return (
<ToDoItem
key={i}
label={i}
onChange={handleChange(i)}
checked={s}
style={{textDecoration: s ? 'line-through' : ''}}/>
)
})}
</div>
);
}
const App = () => {
const [chosen, setChosen] = useState();
const handleChosen = choices => {
setChosen(choices);
}
return (
<div>
<ToDoList items={["Rock", "Paper", "Scissors"]} onChosen={handleChosen} />
{chosen && chosen.length > 0 && <pre>Chosen: {JSON.stringify(chosen,null,2)}</pre>}
</div>
);
}
render(<App />, document.body)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.9.0/umd/react-dom.production.min.js"></script>
Turned out my top-level component Screen is causing this re-render. In my Screen functional component, I have this piece of code before the return
const Content = scroll
? contentProps => {
const { style: contentContainerStyle } = contentProps;
return (
<ScrollView {...contentContainerStyle}>
{contentProps.children}
</ScrollView>
);
}
: View;
return (
<Content>{children}</Content>
)
And it somehow (not sure why) causes the children to re-render every time my state changes.
I fixed it by removing the function and have it simply returning a View
const Content = scroll ? ScrollView : View;