I'm trying to learn how to use react hooks with effects and state.
I have some text with up and down votes on it.
function Overview() {
const [countUp, setCountUp] = useState(0);
const [countDown, setCountDown] = useState(0);
useEffect(() => {
document.title = `You clicked ${countUp} times`;
}, [countUp]);
useEffect(() => {
document.title = `You clicked ${countDown} times`;
}, [countDown]);
return (
<List.Item
key="1"
title="whisperFunding"
description="test"
actions={[
<Button onClick={() => setCountUp(countUp + 1)}><IconText type="like-o" text={countUp} key="list-vertical-like-o" /></Button>,
<Button onClick={() => setCountDown(countDown + 1)}><IconText type="dislike-o" text={countDown} key="list-vertical-like-o" /></Button>,
]}
>
I'm trying to figure out how to save the state so that each time I refresh the page, the previous state is loaded instead of being reset to zero.
Do I need to make a form to do that?
Currently, the up and down clicks work, but if I refresh the page, everything goes back to zero. How do I save the state to preserve for the next time I visit the page?
Is there an example somewhere that I can follow of how to do this? I have plenty of forms in my app but not sure how to integrate one in my list item for the count? or if I can use effect to save the count history.
Here is a crude example of how you can achieve this with the browsers localStorage. You can read the browsers localStorage Here. Since React is a frontend framework you need to save the value somewhere or when you refresh the page the entire app will reload. So you can use a number of different storage devices like localStorage or a database for example. Saving to a database is more permanent and gives you more control because localStorage can be deleted by a user but they rarely do so. If you need a production app that remembers the count long term or you need this count to be more secure you may want to set up an api with nodeJs and mongoDB or something similar but for most cases in frontend frameworks localStorage can be enough and easy to implement. I just made a simple counter you can adjust it to your liking but this should give you a good starting point.
First I used a return statement in your setState functions to get the previous state. Then you can just calculate the count and set the localStorage then return the new count to the state object. To get the initial value from localStorage you can do useEffect with an empty dependency array to run the effect only once when the component first mounts. You can then get the localStorage item and set it to your count. Here is an example.
import React, { useState, useEffect } from "react";
const Counter = () => {
const [count, setCount] = useState(0);
const increase = () => {
setCount(prevCount => {
const newCount = Number(prevCount) + 1;
localStorage.setItem("count", newCount);
return newCount;
});
};
const decrease = () => {
setCount(prevCount => {
const newCount = Number(prevCount) - 1;
localStorage.setItem("count", newCount);
return newCount;
});
};
useEffect(() => {
const initialValue = localStorage.getItem("count");
if (initialValue) setCount(initialValue);
}, []);
// Just to show you the localStorage Value
console.log(localStorage.getItem("count"));
return (
<div>
<button onClick={increase}>Plus</button>
<button onClick={decrease}>Minus</button>
<div>{count}</div>
</div>
);
};
export default Counter;
Related
I am trying to show the number of each item in Drawer Item in react native. for this purpose I created a function which returns the number of items I want. But it does not give me what I want. How can I call this function in Drawer Item title?
function:
const countItems = async () => {
const storedArray = await AsyncStorage.getItem('fav_data').then(favs => {
const count = JSON.parse(favs).length;
return count;
})
// console.log(storedArray)
return storedArray;
}
and here I want to call it:
<DrawerItem title={'test ' + countItems()} accessoryLeft={HeartIcon}
onPress={() => navigation.navigate('Favorites')}/>
You're not awaiting the result of countItems, so it will return undefined at render time. Since the number isn't stored in state, changes to the number won't trigger a re-render of the screen.
Here's an example of the most common way to solve this problem: with state and effect hooks.
const [count, setCount] = useState();
useEffect(() => {
AsyncStorage.getItem('fav_data').then(favs => {
const count = JSON.parse(favs).length;
setCount(count);
})
}, []);
...
<DrawerItem
title={count ?? 'Loading'}
accessoryLeft={HeartIcon}
onPress={() => navigation.navigate('Favorites')}
/>
The useEffect hook has an empty dependency array (the 2nd argument), so it will only run once, on the mount of the component.
If you want to read more about the state and effect hooks, sections 3 and 4 of the following React doc have good explanations: https://reactjs.org/docs/hooks-intro.html
I haven API endpoint, that gives me a random text, on each call. As of now, when the React Component loads for the first time, a call goes through to the API and I get the random text.
The code looks like this. I am using redux for state management.
const RandomQuoteList = ({ todo, isLoading, startLoadingTodos }) => {
useEffect(() => {
startLoadingTodos();
}, []);
const [inputValue, setInputValue] = useState(`HelloThere`);
function changeRandomText(e) {
// const item = e.target.value;
var something = Math.random();
console.log(`changeRandomText clicked + ${something}`);
setInputValue(`changeRandomText clicked + ${something}`);
console.log(inputValue);
}
const loadingMessage = <div>Loading todos...</div>;
const content = (
<GeneralWrapper>
<RandomQuoteItem todo = {todo} inputValue = {inputValue}/>
<Button onClick={changeRandomText}>Get A New Quote</Button>
</GeneralWrapper>
);
return isLoading ? loadingMessage : content;
};
const mapStateToProps = state => ({
isLoading: getTodosLoading(state),
todo: getTodos(state),
});
const mapDispatchToProps = dispatch => ({
startLoadingTodos: () => dispatch(loadTodos()),
});
export default connect(mapStateToProps, mapDispatchToProps)(RandomQuoteList);
Now, I want to be able to use a simple button click to 'refresh' the API call. That way, the API endpoint will be triggered and a fresh new text will get updated.
I have looked at the following stack over flow questions.
React: re render componet after button click
How to refresh React page/component on Button click after POST
ReactJs : How to reload a component onClick
But, I am not getting far. I am able to randomly change the state of a text component, and that is changing the text component. So, I have the random value change part taken care of.
The target component looks something like this. When I click the button on the above component, the below component updates the random text no problem.
const RandomQuoteItem = ({ todo,inputValue }) => {
//set the style for the display.
// const Container = todo.isCompleted ? TodoItemContainer : TodoItemContainerWithWarning;
const Container = TodoItemContainer;
return (
<Container>
{/* this is where you show your API response single items. */}
<h3>{todo.quoteContent}</h3>
<h4>{todo.quoteAuthor}</h4>
<h4>{todo.quoteIdentifierString}</h4>
<p>-------------------------------</p>
<h4>{todo.quoteIdentifierCompadre}</h4>
<h4>{todo.dateTimeOfResponse}</h4>
<h4>{todo.detailsAboutOperation}</h4>
<p>{inputValue}</p>
</Container>
);
}
Now, how do I link this random state change to my RandomQuoteItem state, so, it makes fresh data call?
Based on the comment from rahuuz above, I ended up with this. and it worked.
function changeRandomText(e) {
// const item = e.target.value;
var something = Math.random();
console.log(`changeRandomText clicked + ${something}`);
setInputValue(`changeRandomText clicked + ${something}`);
console.log(inputValue);
startLoadingTodos(); //this specific line solved the problem.
}
I think, it worked in my favour that I already had redux and reducers and all of that hooked up. If that was no the case, this specific solution may not have worked, I think.
Button click calls the startLoadingTodos function, which in turn calls the API and that returns data, updating the redux state, and component also updates.
I am doing a simple exercise to clear my hook concept.
In my current scenario, I need to call the API only if i click on the Call API button and that time the page number should be the current value of Click Me button value.
But currently, on every Click Me button click the API is called.
I have tried with this solution, but can not get the result.
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
console.log(ref.current);
return ref.current;
};
export default function GetAPIData(props) {
const [chars, setChars] = useState([]);
const prevCount = usePrevious(props.id);
console.log(
"Previous value is: " + prevCount,
"Current value is: " + props.id
);
useEffect(() => {
axios
.get(`https://rickandmortyapi.com/api/character/?page=${props.id}`)
.then((res) => {
setChars(res.data.results);
})
.catch((err) => {
alert(err.message);
});
}, [prevCount, props.id]);
return (
<div className="container">
<h3>
<span>Achtung,</span> ein Trend geht um
</h3>
</div>
);
}
Here is the working Sandbox URL for reference.
You are updating the state every time when you click on the button by that the page was re-render and useEffect() is calling the API.
Try to create a new state which only changes when you click on the Call API. By that, your component does not re-render on the change of count.
Might it help You
I wanted to ask if there's a proper way of handling a situation when we have an useEffect that we only care it runs when one variable of the dependency array is changed.
We have a table that has pagination and inputs to filter the content
with a button.
When the user changes the action (inputs) we update this state and
when the user press search we fetch from the api.
When we have results paginated, we then hook on the page and if it
changes we then fetch the corresponding page.
I solved the issue by having the ref of action and checking if the previous value was different from the current value. Though I don't know if this is the best practice
I did something like this.
const FunctionView = () => {
const actionRef = useRef({})
// action object have query params for the api
const fetchData = useCallback((page) => {
// call and api and sets local values
}, [action])
// this hook handle page next or previous
useEffect(() => {
let didCancel = false
if (isEqual(Object.values(action), Object.values(actionRef.current))) {
if (!didCancel) fetchData(page + 1)
}
actionRef.current = action
return () => {
didCancel = true
}
}, [page, fetchData, action])
return (
<>
Components here that changes {action} object, dates and category
<Button onClick={() => fetchData(page)}>Search</Button>
<Table>...</Table>
</>
)
}
I know I can set the dependency array to only page but react lint plugin complains about this so I ended up with warnings.
A common mistake is trying to directly wire the state of forms up to the data they represent. In your example, you DO want to perform the search when action changes. What you're actually trying to avoid is updating action every time one of the inputs changes prior to submit.
Forms are, by nature, transient. The state of the form is simply the current state of the inputs. Unless you really want things to update on every single keystroke, the storage of these values should be distinct.
import { useCallback, useEffect, useState } from 'react'
const Search = () => {
const [action, setAction] = useState({})
const loadData = useCallback(() => {
// call api with values from `action`
}, [action])
useEffect(loadData, [loadData])
return (
<>
<SearchForm setRealData={setAction} />
<Table>...</Table>
</>
)
}
const SearchForm = ({ setRealData }) => {
// don't want to redo the search on every keystroke, so
// this just saves the value of the input
const [firstValue, setFirstValue] = useState('')
const [secondValue, setSecondValue] = useState('')
const handleFirstChange = (event) => setFirstValue(event.target.value)
const handleSecondChange = (event) => setSecondValue(event.target.value)
// now that we have finished updating the form, update the state
const handleSubmit = (event) => {
event.preventDefault()
setRealData({ first: firstValue, second: secondValue })
}
return (
<form onSubmit={handleSubmit}>
<input value={firstValue} onChange={handleFirstChange} />
<input value={secondValue} onChange={handleSecondChange} />
<button type="submit">Search</button>
</form>
)
}
I'm trying to fetch data with a custom React hook, based on the current router parameters.
https://stackblitz.com/edit/react-fetch-router
What it should do:
On first load, check if URL contains an ID...
If it does, fetch a todo with that ID
If it does not, fetch a todo with a random ID & add ID to url
On fetch button clicks...
Fetch a todo with a random ID & add ID to url
What is wrong:
Watching the console or inspector network tab, you can see that it's firing several fetch requests on each click - why is this and how should this be done correctly?
Since you used history.push on handleClick, you will see multiple requests being sent as you are using history.push on click handler which will cause a re-render and make use use random generated url as well as also trigger the fetchTodo function.
Now a re-render will occur which is cause a randomised id to be generated. and passed onto useTodo hook which will lead to the fetchTodo function being called again.
The correct solution for you is to set the random todo id param on handleClick and also avoid unnecessary url updates
const Todos = () => {
const history = useHistory();
const { id: todoId } = useParams();
const { fetchTodo, todo, isFetching, error } = useTodo(todoId);
const isInitialRender = useRef(true);
const handleClick = () => {
const todoId = randomMax(100);
history.push(`/${todoId}`);
};
useEffect(() => {
history.push(`/${todoId}`);
}, []);
useEffect(() => {
if(!isInitialRender.current) {
fetchTodo();
} else {
isInitialRender.current = false
}
}, [todoId])
return (
<>
<button onClick={handleClick} style={{marginBottom: "10px"}}>Fetch a todo</button>
{isFetching ? (
<p>Fetching...</p>
) : (
<Todo todo={todo} color={todo.color} isFetching={isFetching} />
)
}
</>
);
};
export default Todos;
Working demo