React js - unable to set state of variable properly - javascript

I have a functional component in React and Im having some troubles setting the state of a variable correctly. I have a Pagination component which I am using for my website and I want to know the page I am on when I click page 2 or 3 etc. I decided to set the state of the page with the handlepage function if u see below which sets the page according to which page I press. However in my other function which is onChangePage, I cannot get the correct page value when I console log which I thought should work since Ive already set the state of the page with the onchange in the pagination component. Maybe this is happening because in the pagination component I am calling both functions at the same time that causes the problem. Please tell me how to solve this, it would really help alot I need the correct value of the page in onChangePage.
function Build() {
const [page, setPage] = React.useState(1);
const handlePage = (event, value) => {
setPage(value);
}
// Function for pagination, calls redux
const onChangePage = (name, val, page) => {
console.log(page)
console.log("PAGING HERE")
dispatch(getAllPreownedCars(6, dataOptions.skip, filters, dataOptions.searchText, [val]));
}
return(
<Pagination
count={5}
onClick={() => onChangePage("Price (Lowest to Highest)", "selling_Price ASC", page)}
page={page}
onChange={handlePage} />
);
}
export Build;

React state updates are asynchronous and processed between render cycles. You are likely executing both callbacks simultaneously, so the enqueued state update from one isn't going to be available in the other.
Is there a way to pass these 2 strings into handlePage?
Yes, you can proxy the onChange event object and value to your handler. Think something along the lines of an anonymous function as follows:
const handlePage = (event, page, string1, string2) => {....}
...
onChange={(event, page) => handlePage(event, page, string1, string2)}
Below I've written handlePage as a curried function to first consume the two strings you want passed and returning a callback function to consume the onChange event object and new page value.
const handlePage = (name, val) => (event, page) => {
console.log(page);
console.log("PAGING HERE");
setPage(page);
dispatch(
getAllPreownedCars(6, dataOptions.skip, filters, dataOptions.searchText, [val])
);
};
Code
function Build() {
const [page, setPage] = React.useState(1);
const handlePage = (name, val) => (event, page) => {
console.log(page);
console.log("PAGING HERE");
setPage(page);
dispatch(
getAllPreownedCars(6, dataOptions.skip, filters, dataOptions.searchText, [val])
);
};
return (
<Pagination
count={5}
page={page}
onChange={handlePage("Price (Lowest to Highest)", "selling_Price ASC")}
/>
);
}

Related

React Native + Context + FlashList wont re-render with Context update + extraData updating

The problem: I have a FlashList that uses React Context to fill in the data (the data is an array of objects that renders a View) but when I update the context and the extraData prop for FlashList, the list does not re-render, or re-renders sometimes, or takes multiple events to actually re-render.
The Code:
// Many imports, they are all fine though
export default () => {
// Relevant context.
const {
cardsArray,
cardsArrayFiltered,
updateCardsArray,
updateCardsArrayFiltered
} = useContext(AppContext);
// Relevant state.
const [didUpdateCards, setDidUpdateCards] = useState(false);
const [cardsFilters, setCardsFilters] = useState([]);
// Relevant refs.
const flatListRef = useRef(null);
// Example effect on mount
useEffect(() => {
setInitialAppState();
}, []);
// Effect that listen to changing on some data that update the context again
useEffect(() => {
const newCardsArray = doSomeFiltering(cardsArray, cardsFilters);
updateCardsArrayFiltered(newCardsArray);
setDidUpdateCards(!didUpdateCards);
}, [cardsFilters]);
// Example of promisey function that sets the initial context.
const setInitialAppState = async () => {
try {
const newCardsArray = await getPromiseyCards();
updateCardsArrayFiltered(newCardsArray);
updateCardsArray(newCardsArray);
} catch ( err ) {
console.debug( err );
}
}
// Renderer for the list item.
const renderListItem = useCallback((list) => <Card key={list.index} card={list.item} />, []);
// List key extractor.
const listKeyExtractor = useCallback((item) => item.id, []);
return (
<FlashList
ref={flatListRef}
data={cardsArrayFiltered}
extraData={didUpdateCards}
keyExtractor={listKeyExtractor}
renderItem={renderListItem}
showsVerticalScrollIndicator={false}
estimatedItemSize={Layout.window.height}
/>
);
}
Notes:
What I did not write all out is the function, logic, view to update cardsFilters however the above effect IS running when it changes.
Moreover, this line here, const newCardsArray = doSomeFiltering(cardsArray, cardsFilters); does indeed return the proper updated data.
What's going on here? I am updating the extraData prop with that didUpdateCards state when the context changes which I thought was the requirement to re-render a FlatList/FlashList.
It looks like object being passed as extraData is a boolean. This means that if the previous value was true, setting it as true again wouldn't count as a change. Instead use an object and update it when you want list to update.
To try just set extraData={{}}. if everything works as expected it means that your update logic has some problem.

How to delegate responsibilities to each component correctly in React?

I have a hook, which basically renders a Loading Indicator at the right side of my Stack Header.
Here is how it looks like:
export default function useHeaderRightLoadingIndicator(loading = false) {
const navigation = useNavigation();
const renderLoading = useCallback(() => {
if (!loading) return null;
return (
<Loading
size={20}
style={styles.loadingIndicator}
/>
);
}, [loading]);
useEffect(() => {
navigation.setOptions({
headerRight: renderLoading,
});
}, [navigation, renderLoading]);
}
Now, I want to use it during a delete operation. Basically, I have a screen with a lot of erasable items rendered in a FlatList.
The screen contains the stateful array of data... and pass it to the FlatList component.
In order to avoid repeating my self each time I wanna delete the same item (but in a different screen), I have decided to move the responsibility of the deletion to the item itself (I mean, to the FlatList's item). For this, I have been forced to use a context in order to update the graphical interface without having to pass the "setState" node by node.
So... I have the following:
function MyScreen() {
const statefulData = useData(); // consuming context...
return <CustomFlatList data={statefulData} />
}
...
function ErasableItem({ id }) {
const { isLoading, error, deleteItem } = useDeleteItem(id);
return <Text onPress={deleteItem}>Delete!</Text>;
}
Inside the logic of useDeleteItem, I just make an api call and update my context in order to update the UI (delete the item from the list).
This has sense to me... I don't have a super screen which does everything, instead, each node does its own stuff.
But... what if I wanna use the useHeaderRightLoadingIndicator() I described at the beginning? As I have delegated the deletion responsibility to an unmountable component, I will not be able to stop loading it. I mean, if I do:
function ErasableItem({ id }) {
const { isLoading, error, deleteItem } = useDeleteItem(id);
useHeaderRightLoadingIndicator(isLoading);
return <Text onPress={deleteItem}>Delete!</Text>;
}
As the item will be unmounted from the screen, the useEffect of the useHeaderRightLoadingIndicator will not be called the last time, when isLoading toggles from true to false.
In order to fix that, I will have to run a cleanup function, like this:
export default function useHeaderRightLoadingIndicator(loading = false) {
const navigation = useNavigation();
const renderLoading = useCallback(() => {
if (!loading) return null;
return (
<Loading
size={20}
style={styles.loadingIndicator}
/>
);
}, [loading]);
useEffect(() => {
navigation.setOptions({
headerRight: renderLoading,
});
return () => {
// cleanup
navigation.setOptions({ headerRight: null });
};
}, [navigation, renderLoading]);
}
Something that makes me doubt, since if I had delegated the delete operation to the screen, which is not unmountable, I would not have any problem and I would not have to execute that cleanup function.
What is the logic to follow when delegating responsibilities? Is my delegation incorrect in React?

Have a custom hook run an API call only once in useEffect

I have a custom hook in my React application which uses a GET request to fetch some data from the MongoDB Database. In one of my components, I'm reusing the hook twice, each using different functions that make asynchronous API calls.
While I was looking at the database logs, I realized each of my GET requests were being called twice instead of once. As in, each of my hooks were called twice, making the number of API calls to be four instead of two. I'm not sure why that happens; I'm guessing the async calls result in re-renders that aren't concurrent, or there's somewhere in my component which is causing the re-render; not sure.
Here's what shows up on my MongoDB logs when I load a component:
I've tried passing an empty array to limit the amount of time it runs, however that prevents fetching on reload. Is there a way to adjust the custom hook to have the API call run only once for each hook?
Here is the custom hook which I'm using:
const useFetchMongoField = (user, id, fetchFunction) => {
const [hasFetched, setHasFetched] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
if (!user) return;
try {
let result = await fetchFunction(user.email, id);
setData(result);
setHasFetched(true);
} catch (error) {
setError(error.message);
}
};
if (data === null) {
fetchData();
}
}, [user, id, fetchFunction, data]);
return { data, hasFetched, error };
};
This is one of the components where I'm re-using the custom hook twice. In this example, getPercentageRead and getNotes are the functions that are being called twice on MongoDB (two getPercentageRead calls and two getNotes calls), even though I tend to use each of them once.
const Book = ({ location }) => {
const { user } = useAuth0();
const isbn = queryString.parse(location.search).id;
const { data: book, hasFetched: fetchedBook } = useFetchGoogleBook(isbn);
const { data: read, hasFetched: fetchedPercentageRead } = useFetchMongoField(
user,
isbn,
getPercentageRead
);
const { data: notes, hasFetched: fetchedNotes } = useFetchMongoField(
user,
isbn,
getNotes
);
if (isbn === null) {
return <RedirectHome />;
}
return (
<Layout>
<Header header="Book" subheader="In your library" />
{fetchedBook && fetchedPercentageRead && (
<BookContainer
cover={book.cover}
title={book.title}
author={book.author}
date={book.date}
desc={book.desc}
category={book.category}
length={book.length}
avgRating={book.avgRating}
ratings={book.ratings}
language={book.language}
isbn={book.isbn}
username={user.email}
deleteButton={true}
redirectAfterDelete={"/"}
>
<ReadingProgress
percentage={read}
isbn={book.isbn}
user={user.email}
/>
</BookContainer>
)}
{!fetchedBook && (
<Wrapper minHeight="50vh">
<Loading
minHeight="30vh"
src={LoadingIcon}
alt="Loading icon"
className="rotating"
/>
</Wrapper>
)}
<Header header="Notes" subheader="All your notes on this book">
<AddNoteButton
to="/add-note"
state={{
isbn: isbn,
user: user,
}}
>
<AddIcon color="#6b6b6b" />
Add Note
</AddNoteButton>
</Header>
{fetchedNotes && (
<NoteContainer>
{notes.map((note) => {
return (
<NoteBlock
title={note.noteTitle}
date={note.date}
key={note._noteID}
noteID={note._noteID}
bookID={isbn}
/>
);
})}
{notes.length === 0 && (
<NoNotesMessage>
You don't have any notes for this book yet.
</NoNotesMessage>
)}
</NoteContainer>
)}
</Layout>
);
};
The way you have written your fetch functionality in your custom hook useFetchMongoField you have no flag to indicate that a request was already issued and you are currently just waiting for the response. So whenever any property in your useEffect dependency array changes, your request will be issued a second time, or a third time, or more. As long as no response came back.
You can just set a bool flag when you start to send a request, and check that flag in your useEffect before sending a request.
It may be the case that user and isbn are not set initially, and when they are set they each will trigger a re-render, and will trigger a re-evalution of your hook and will trigger your useEffect.
I was able to fix this issue.
The problem was I was assuming the user object was remaining the same across renders, but some of its properties did in fact change. I was only interested in checking the email property of this object which doesn't change, so I only passed user?.email to the dependency array which solved the problem.

Select specific checkbox among an array of checkboxes

I have data coming from getTasks() and I store it on tasks
I created a component for each task with their data with a checkbox for each task.
I want to click a checkbox and display only the one I clicked, not them all.
I am using React Native. How can I do this?
thanks.
export default () => {
const [tasks, setTasks] = React.useState([]);
const [checked, setChecked] = React.useState(false);
React.useEffect(() => {
(async () => {
const response = await getTasks();
setTasks(response);
})();
}, []);
return tasks.map(({id, title, description, date}) => (
<View key={id} style={styles.task}>
<CheckBox
value={checked}
onValueChange={() => {
setChecked((prevState) => !prevState);
}}
/>
<View style={styles.taskTextContainer}>
<Text>{title}</Text>
<Text>{description}</Text>
<Text>{date}</Text>
</View>
</View>
));
};
You're passing the same value={checked} to all checkboxes. You can create an array of booleans to accomplish that.
You have two problems in that code, first is the problem you described, the second one is the way you're changing the state of that checkbox.
About the first problem, in order to display the respective state for each task, you should also include a property that defines if the task is marked or not.
First, verify the name of the property thats set if the task is done or not, I'll give the name here as done and done is of type boolean.
// notice the done property
return tasks.map(({id, title, description, date, done}) => (
Then, you should use done to change the checked state of your Checkbox like this:
<Checkbox
value={done}
...
/>
Now you should change a little how you change that checked state.
First, you need to write a function to change the value of done, to fase or true, according by the Checkbox component's state and that's really simple:
const onCheckboxChange = (taskId) => {
// first, let's find the index of the changed task
const index = tasks.findIndex(({ id }) => id === taskId)
// next, let's get the data by the index
const data = tasks[index]
// now, we can just toggle done value like this
data.done = !data.done
// then, let's assign updated data to its own index in tasks array
tasks[index] = data
// finally, we can update the tasks state using a copy of changed tasks
setTasks([...tasks])
}

Don't understand how to update React hook

I've tried to ask this ungooglable to me question dozens of times. I've made almost the simpliest example possible to ask this question now.
I change the value of the hook in the handleChange method. But then console.log always shows previous value, not new one. Why is that?
I need to change the value of the hook and then instead of doing console.log use it to do something else. But I can't because the hook always has not what I just tried to put into it.
const options = ["Option 1", "Option 2"];
export default function ControllableStates() {
const [value, setValue] = React.useState(options[0]);
const handleChange = val => {
setValue(val);
console.log(value);
};
return (
<div>
<div>{value}</div>
<br />
<Autocomplete
value={value}
onChange={(event, newValue) => {
handleChange(newValue);
}}
options={options}
renderInput={params => (
<TextField {...params} label="Controllable" variant="outlined" />
)}
/>
</div>
);
}
You can try it here.
https://codesandbox.io/s/awesome-lumiere-y2dww?file=/src/App.js
I believe the problem is that you are logging the value in the handleChange function. Console logging the value outside of the function logs the correct value. Link: https://codesandbox.io/s/async-fast-6y71b
Hooks do not instantly update the value you want to update, as you might have expected with classes (though that wasn't guaranteed either)
State hook, when calling setValue will trigger a re-render. In that new render, the state will have the new value as you expected. That's why your console.log sees the old value.
Think of it as in each render, the state values are just local variables of that component function call. And think as the result of your render as a result of your state + props in that render call. Whenever any of those two changes (the props from your parent component; the state, from your setXXX function), a new render is triggered.
If you move out the console.log outside of the callback handler (that is, in the body of your rendered), there you will see in the render that happens after your interaction that the state is logged correctly.
In that sense, in your callbacks events from interactions, you just should worry about updating your state properly, and the next render will take care to, given the new props/state, re-render the result
The value doesn't "change" synchronously - it's even declared with a const, so even the concept of it changing inside the same scope doesn't make sense.
When changing state with hooks, the new value is seen when the component is rerendered. So, to log and do stuff with the "new value", examine it in the main body of the function:
const ControllableStates = () => {
const [value, setValue] = React.useState(options[0]);
const handleChange = val => {
setValue(val);
};
// ADD LOG HERE:
console.log('New or updated value:', value);
return (
<div>
<div>{value}</div>
<br />
<Autocomplete
value={value}
onChange={(event, newValue) => {
handleChange(newValue);
}}
options={options}
renderInput={params => (
<TextField {...params} label="Controllable" variant="outlined" />
)}
/>
</div>
);
}
You're printing out the old value in handleChange, not the new val.
i.e.
const handleChange = val => {
setValue(val);
console.log(value);
};
Should be:
const handleChange = val => {
setValue(val);
console.log(val);
};
Actually, lets get a little back and see the logic behind this scenario.
You should use the "handleChange" function ONLY to update the state hook, and let something else do the logic depends on that state hook value, which is mostly accomplished using "useEffect" hook.
You could refactor your code to look like this:
const handleChange = val => {
setValue(val);
};
React.useEffect(() => {
console.log(value);
// do your logic here
}, [value])
So I think that the main problem is that you're not understanding how React
deals with components and states.
So, I'll vastly simplify what React does.
React renders a new component and remembers it's state, it's inputs (aka
props) and it's the state and inputs of the children.
If at any given point an input changes or a state changes, React will render
the component again by calling the component function.
Consider this:
function SomeComponent(text) {
return (<div>The <i>text</i> prop has the value {text}</div>)
}
Let's say the initial prop value is "abc", React will call SomeComponent("abc"), then the function returns
<div>The <i>text</i> prop has the value abc</div> and React will render that.
If the prop text does not change, then React does nothing anymore.
Now the parent component changes the prop to "def", now React will call
SomeComponent("def") and it will return
<div>The <i>text</i> prop has the value def</div>, this is different from
last call, so React will update the DOM to reflect the change.
Now let's introduce state
function SomeComponent() {
const [name, setName] = React.useState("John")
function doSomething()
{
alert("The name is " + name)
}
return (
<p>Current name: {name}</p>
<button onClick={() => setName("Mary")}>Set name to Mary</button>
<button onClick={() => setName("James")}>Set name to James</button>
<button onClick={() => doSomething()}>Show current name</button>
)
}
So here React will call SomeComponent() and render the name John and the 3
button. Note that the value of the name variable does not change during the
current execution, because it's declared as const. This variable only reflects
the latest value of the state.
When you press the first button, setName() is executed. React will internally store
the new value for the state and because of the change of state, it will render
the component again, so SomeComponent() will be called once again. Now the variable name will
reflect again the latest value of the state (that's what useStatedoes), so in this case Mary. React
will realize that the DOM has to be updated and it prints the name Mary.
If you press the third button, it will call doSomething() which will print the
latest value of the name variable because every time React calls
SomeComponent(), the doSomething() function is created again with the latest
value of name. So once you've called setName(), you don't need to do
anything special to get the new value. React will take care of calling the
component function again.
So when you don't use class components but function components, you have to think
differently: the function gets called all the time by React and at any single
execution it reflects the latest state at that particular point in time. So when you
call the setter of a useState hook, you know that the component function will
be called again and useState will return the new value.
I recommend that you read this article, also read again Components and
Props from the React documentation.
So how should you do proceed? Well, like this:
const options = ["Option 1", "Option 2"];
export default function ControllableStates() {
const [value, setValue] = React.useState(options[0]);
const handleChange = val => {
setValue(val);
console.log(value);
};
const handleClick = () => {
// DOING SOMETHING WITH value
alter(`Now I'm going to do send ${value}`);
}
return (
<div>
<div>{value}</div>
<br />
<Autocomplete
value={value}
onChange={(event, newValue) => {
handleChange(newValue);
}}
options={options}
renderInput={params => (
<TextField {...params} label="Controllable" variant="outlined" />
)}
/>
<button type="button" onClick={handleClick}>Send selected option</button>
</div>
);
}
See CodeSandbox.

Categories