I am using React as the recommended function.
But even if I put the state value received from useSelector into useEffect's dep, useEffect doesn't execute as intended.
When submitLike is executed, the detailPost of the state value is updated, but useEffect is not executed except for the first time.
Can you suggest me a solution ?
Below is my tsx file and reducer
post.tsx(page)
const Post = () => {
const dispatch = useDispatch();
const detailPost = useSelector((store: RootState) => store.post.detailPost);
const [post, setPost] = useState({ ...detailPost });
const [isLiked, setIsLiked] = useState(
{ ...detailPost }.liker?.split(',').filter((v: string) => +v === me.id).length || 0,
);
const submitLike = () => {
if (isLiked) dispatch(UNLIKE_POST_REQUEST({ userId: me.id, postId: detailPost.id }));
else dispatch(LIKE_POST_REQUEST({ userId: me.id, postId: detailPost.id }));
};
useEffect(() => {
loadPostAPI(window.location.href.split('/')[4])
.then((res) => {
setPost(res.data);
const currentLiked = res.data.liker?.split(',').filter((v: string) => +v === me.id).length || 0;
setIsLiked(currentLiked);
return currentLiked;
})
.catch((error) => console.log(error));
}, [detailPost]);
return (
...
post.User.nickname
post.like
...
);
};
export default Post;
post.ts(reducer)
const Post = (state = initialState, action: any) => {
switch (action.type) {
...
case LIKE_POST_REQUEST:
return { ...state, likePostLoading: true, likePostDone: false, likePostError: null };
case LIKE_POST_SUCCESS: {
const posts: any[] = [...state.mainPosts];
const post = posts.find((v) => v.id === action.data.postId);
if (post.liker) post.liker += `,${action.data.userId}`;
else post.liker = `${action.data.userId}`;
post.like += 1;
return { ...state, likePostLoading: false, likePostDone: true, likePostError: null, detailPost: post };
}
case UNLIKE_POST_SUCCESS: {
const posts: any[] = [...state.mainPosts];
const post = posts.find((v) => v.id === action.data.postId);
const liker = post.liker.split(',');
const idx = liker.find((v: string) => +v === action.data.userId);
liker.splice(idx, 1);
post.liker = liker.join('');
post.like -= 1;
return { ...state, unlikePostLoading: false, unlikePostDone: true, unlikePostError: null, detailPost: post };
}
default:
return state;
...
}
};
export default Post;
And when I click refresh, the post values become undefined and an error occurs.
I also want to solve this problem with useEffect.
Related
I'm trying to save to localstorage this cart value
const addToCart = (payload) => {
setState({
...state,
cart: [...state.cart, payload ]
})
}
Using this code as initialState
import { useState } from "react"
import { useLocalStorage } from "./useLocalStorage"
const useInitialState = () => {
const [inc, setInc] = useLocalStorage("favCounter", false)
const initialState = {
favCounter: inc,
cart: []
}
const [state, setState] = useState(initialState)
const incrementFav = () => {
setState({
...state,
favCounter: state.favCounter + 1
})
setInc(state.favCounter + 1)
}
const decrementFav = () => {
setState({
...state,
favCounter: state.favCounter - 1
})
setInc(state.favCounter - 1)
}
const addToCart = (payload) => {
setState({
...state,
cart: [...state.cart, payload ]
})
}
return {
state,
incrementFav,
decrementFav,
addToCart
}
}
export default useInitialState
And this code as custom hook "useLocalStorage"
import { useState } from 'react'
export function useLocalStorage (key, initialValue) {
const [storedValue, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key)
return item !== null ? JSON.parse(item) : initialValue
} catch (e) {
return initialValue
}
})
const setLocalStorage = value => {
try {
window.localStorage.setItem(key, JSON.stringify(value))
setValue(value)
} catch (e) {
console.log(e)
}
}
return [storedValue, setLocalStorage]
}
And I don´t know how to store the cart (array) value to localstorage as I did it with "favcounter" stored value.
const [localCart, setLocalCart] = useLocalStorage("cartState", [])
const addToCart = (payload) => {
setState((prevState) => {
const newState = { ...prevState, cart: [...prevState.cart, payload ]}
setLocalCart(newState.cart)
return newState
})
}
Actions
import { FETCH_BLOG, FETCH_BLOG_ERROR, FETCH_BLOG_LOADING } from "../constants/blogActionTypes"
const initialState = {
blogs: [],
error: '',
loading: false,
allBlogs: []
}
// eslint-disable-next-line import/no-anonymous-default-export
export default (blogs = initialState, action) => {
switch (action.type) {
case FETCH_BLOG_LOADING:
return {
blogs: [...blogs.blogs],
loading: true,
error: ''
};
case FETCH_BLOG_ERROR:
return {
blogs: [...blogs.blogs],
loading: false,
error: action.payload
};
case FETCH_BLOG:
return {
blogs: [...action.payload, ...blogs.blogs],
loading: false,
error: ''
};
default: return blogs;
}
}
Reducers
export const fetchBlogs = (data) => async (dispatch) =>{
dispatch({ type: FETCH_BLOG_LOADING, payload: true })
fetch('http://localhost:5000/blog?show=' + data, {
method: 'GET',
headers: {
authorization: userData.token
}
})
.then(res => res.json())
.then(data => {
if (data.message) {
dispatch(fetchBlogsError(data.message))
} else {
dispatch({ type: FETCH_BLOG, payload: data })
}
})
}
React
const [fetchData, setFetchData] = useState(0);
const showData = () => {
setFetchData(fetchData + 10)
}
const dispatch = useDispatch();
const { loading, error, blogs, } = useSelector(state => state.blogs)
const getData = useCallback( () => {
dispatch(fetchBlogs(fetchData))
}, [fetchData])
useEffect(() => {
getData()
}, [getData])
On the first render, I fetch 10 items.after clicking on load more I fetch another 10 data from database. On the blog component it's fine but after go back to the home page and get back to the blog page; the blog items duplicates. How to fix this duplicate issue>
There are two issues here which are inter-related, you possibly don't need to address #2 depending on how you address #1.
You should add a condition to your thunk action so that you don't fetch a page that you have previously fetched.
You should separate your blog items by page so that you aren't always appending the newest items at the end of the array if you fetch page 1 twice.
Sidenote: [...blogs.blogs] is unnecessary because there is reason to clone properties which you aren't changing.
I'm confused by your API calls. It looks like /blog?show=20 is getting posts 21-30 but I would think based on the name show that it would be posts 1-20.
Using position indexes:
import { createAsyncThunk, createReducer } from "#reduxjs/toolkit";
export const fetchBlogs = createAsyncThunk(
"blogs/fetchBlog",
async (startIndex, { getState, rejectWithValue }) => {
const res = await fetch("http://localhost:5000/blog?show=" + startIndex, {
method: "GET",
headers: {
// where does userData come from ??
authorization: userData.token
}
});
const data = await res.json();
if (data.message) {
rejectWithValue(data.message);
} else {
return data;
}
},
{
condition: (startIndex, { getState }) => {
const { blogs } = getState();
// cancel if loading of if first post on paage is loaded
if (blogs.loading || blogs.blogs[startIndex]) {
return false;
}
}
}
);
const initialState = {
blogs: [],
error: "",
loading: false
};
export default createReducer(initialState, (builder) =>
builder
.addCase(fetchBlogs.pending, (state) => {
state.loading = true;
state.error = "";
})
.addCase(fetchBlogs.rejected, (state, action) => {
state.loading = false;
state.error = action.payload ?? action.error;
})
.addCase(fetchBlogs.fulfilled, (state, action) => {
const startIndex = action.meta.arg;
const newBlogs = action.payload;
// insert in the array at the correct position
state.blogs.splice(startIndex, newBlogs.length, newBlogs);
})
);
Using separated pages:
import { createAsyncThunk, createReducer, createSelector } from "#reduxjs/toolkit";
export const fetchBlogs = createAsyncThunk(
"blogs/fetchBlog",
async (pageNumber, { getState, rejectWithValue }) => {
const startIndex = 10 * (pageNumber - 1);
const res = await fetch("http://localhost:5000/blog?show=" + startIndex, {
method: "GET",
headers: {
// where does userData come from ??
authorization: userData.token
}
});
const data = await res.json();
if (data.message) {
rejectWithValue(data.message);
} else {
return data;
}
},
{
condition: (pageNumber, { getState }) => {
const { blogs } = getState();
// cancel if loading of if there is a property for this page
if (blogs.loading || blogs.blogs[pageNumber]) {
return false;
}
}
}
);
const initialState = {
//arrays keyed by page number
blogs: {},
error: "",
loading: false
};
export default createReducer(initialState, (builder) =>
builder
.addCase(fetchBlogs.pending, (state) => {
state.loading = true;
state.error = "";
})
.addCase(fetchBlogs.rejected, (state, action) => {
state.loading = false;
state.error = action.payload ?? action.error;
})
.addCase(fetchBlogs.fulfilled, (state, action) => {
const pageNumber = action.meta.arg;
state.blogs[pageNumber] = action.payload;
})
);
// want to flatten the blogs array when selecting
// create a memoized selector
export const selectBlogs = createSelector(
state => state.blogs,
(blogsState) => ({
...blogsState,
blogs: Object.values(blogsState.blogs).flat(1)
})
)
With component:
export default () => {
const [pageNumber, setPageNumber] = useState(1);
const showNext = () => {
setPageNumber((page) => page + 1);
};
const dispatch = useDispatch();
const { loading, error, blogs } = useSelector(selectBlogs);
useEffect(() => {
dispatch(fetchBlogs(pageNumber));
}, [dispatch, pageNumber]);
In the categories component, I render a random image from each category. I also added a onClick event to each image. When the image is clicked, it will dispatch the action getCategory(target.alt) and the DOM will render the products from the clicked category. The problem I got is that every time I clicked a random category image, the DOM will re-render and new random images will appear on the DOM. How do I prevent this re-render? Below is my codes.
const Categories = ({selectedCategory}) => {
const isLoading = useSelector(state => state.productsReducer.isLoading);
const productsByCategory = useSelector(state =>
state.productsReducer.productsByCategories);
const getRandomProductsByCategory = () => {
const randomProducts = []
for(let categories in productsByCategory) {
const randomCategory = productsByCategory[categories][getRandomIndex(productsByCategory[categories].length)];
productsByCategory[categories].map(category => {
if(category === randomCategory) {
randomProducts.push(category)
}
})
}
return randomProducts;
}
return (
<div class='categories-container'>
{getRandomProductsByCategory().map(randomProduct => (
<img onClick={selectedCategory} src={randomProduct.image} />}
</div>
)
}
function App() {
const dispatch = useDispatch();
const category = useSelector(state => state.productsReducer.category)
useEffect(() => {
dispatch(getProducts())
}, [dispatch])
const handleCategoryClick = ({target}) => {
return dispatch(getCategory(target.alt))
}
return (
<>
{/* <ProductsList /> */}
<Categories selectedCategory={handleCategoryClick} />
{category.map(product => <img src={product.image} />)}
</>
)
}
const populateProductsStarted = () => ({
type: 'POPULATE_PRODUCTS/fetchStarted'
})
const populateProductsSuccess = products => ({
type: 'POPULATE_PRODUCTS/fetchSuccess',
payload: products
})
const populateProductsFailed = error => ({
type: 'POPULATE_PRODUCTS/fetchFailed',
error
})
export const getCategory = (category) => ({
type: 'GET_CATEGORY',
category
})
const getProducts = () => async dispatch => {
dispatch(populateProductsStarted())
try {
const response = await fetch(url)
if(response.ok) {
let jsonResponse = await response.json();
return dispatch(populateProductsSuccess(jsonResponse))
}
} catch (err) {
dispatch(populateProductsFailed(err.toString()))
}
}
const initialState = {
isLoading: false,
isError: null,
allProducts: [],
productsByCategories: {},
category: []
}
const productsReducer = (state=initialState, action) => {
switch(action.type) {
case 'POPULATE_PRODUCTS/fetchStarted':
return {
...state,
isLoading: true
}
case 'POPULATE_PRODUCTS/fetchSuccess':
return {
...state,
isLoading: false,
allProducts: action.payload,
productsByCategories: action.payload.reduce((accumulatedProduct, currentProduct) => {
accumulatedProduct[currentProduct.category] = accumulatedProduct[currentProduct.category] || [];
accumulatedProduct[currentProduct.category].push(currentProduct);
return accumulatedProduct;
}, {})
}
case 'POPULATE_PRODUCTS/fetchFailed':
return {
...state,
isError: action.error
}
case 'GET_CATEGORY':
return {
...state,
category: state.allProducts.filter(product => product.category === action.category)
}
default:
return state
}
}
One way to achieve this is through memoization provided by React's useMemo.
const images = React.useMemo(getRandomProductsByCategory().map(randomProduct => (
<img onClick={selectedCategory} src={randomProduct.image} />, [productsByCategory])
return (
<div class='categories-container'>
{images}
</div>
)
This will keep the srcs consistent across re-renders.
I'm trying to update an object property previously declared in a useState hook for form values and save it in localstorage. Everything goes well, but localstorage is saving date property empty all the time, I know that it must be because of asynchrony but I can't find the solution. This is my code. I'm newbie with React hooks. Lot of thanks!
const [formValues,setformValues] = useState(
{
userName:'',
tweetText:'',
date:''
}
)
const getlocalValue = () => {
const localValue = JSON.parse(localStorage.getItem('tweetList'));
if(localValue !== null){
return localValue
} else {
return []
}
}
const [tweetList,setTweetList] = useState(getlocalValue());
const handleInput = (inputName,inputValue) => {
setformValues((prevFormValues) => {
return {
...prevFormValues,
[inputName]:inputValue
}
})
}
const handleForm = () => {
const {userName,tweetText} = formValues;
if(!userName || !tweetText) {
console.log('your tweet is empty');
} else {
setformValues(prevFormValues => {
return {
...prevFormValues,
date:getCurrentDate() //this is not updating in local
}
})
setTweetList(prevTweets => ([...prevTweets, formValues]));
toggleHidden(!isOpen)
}
}
console.log(formValues) //but you can see changes outside the function
useEffect(() => {
localStorage.setItem('tweetList', JSON.stringify(tweetList));
}, [tweetList]);
In this case the issue is because the handleForm that was called still only has access to the formValues state at the time it was called, rather than the new state. So, the easiest way to handle this is to just update the formValues, setFormValues, and then setTweetList based on the local copy of the updated formValues.
const handleForm = () => {
const {userName,tweetText} = formValues;
if(!userName || !tweetText) {
console.log('your tweet is empty');
} else {
const updatedFormValues = {...formValues,date:getCurrentDate()};
setformValues(updatedFormValues)
setTweetList(prevTweets => ([...prevTweets, updatedFormValues]));
toggleHidden(!isOpen)
}
}
Since there's issues with concurrency here: i.e. you can't guarantee an update to the state of both formValues and tweetList with the latest data. Another option is useReducer instead of the two separate state variables because they are related properties and you'd be able to update them based off of each other more easily.
As an example of making more complicated updates with reducers, I added a 'FINALIZE_TWEET' action that will perform both parts of the action at once.
const Component = () => {
const [{ formValues, tweetList }, dispatch] = useReducer(
reducer,
undefined,
getInitState
);
const handleInput = (inputName, inputValue) => {
dispatch({ type: 'SET_FORM_VALUE', payload: { inputName, inputValue } });
};
const handleForm = () => {
const { userName, tweetText } = formValues;
if (!userName || !tweetText) {
console.log('your tweet is empty');
} else {
dispatch({ type: 'SET_FORM_DATE' });
dispatch({ type: 'PUSH_TO_LIST' });
// OR
// dispatch({type: 'FINALIZE_TWEET'})
toggleHidden(!isOpen);
}
};
console.log(formValues); //but you can see changes outside the function
useEffect(() => {
localStorage.setItem('tweetList', JSON.stringify(tweetList));
}, [tweetList]);
return <div></div>;
};
const getlocalValue = () => {
const localValue = JSON.parse(localStorage.getItem('tweetList'));
if (localValue !== null) {
return localValue;
} else {
return [];
}
};
function getInitState() {
const initialState = {
formValues: {
userName: '',
tweetText: '',
date: '',
},
tweetList: getlocalValue(),
};
}
function reducer(state, action) {
switch (action.type) {
case 'SET_FORM_VALUE':
return {
...state,
formValues: {
...state.formValues,
[action.payload.inputName]: action.payload.inputValue,
},
};
case 'SET_FORM_DATE':
return {
...state,
formValues: {
...state.formValues,
date: getCurrentDate(),
},
};
case 'PUSH_TO_LIST':
return {
...state,
tweetList: [...state.tweetList, state.formValues],
};
case 'FINALIZE_TWEET': {
const newTweet = {
...state.formValues,
date: getCurrentDate(),
};
return {
...state,
formValues: newTweet,
tweetList: [...state.tweetList, newTweet],
};
}
default:
return state;
}
}
I am really struggling to understand why there are so many use**Async hook libraries in react when hooks cannot be used in event handlers.
If I look at this code:
import { useEffect, useReducer } from 'react';
const initialState = {
started: false,
pending: true,
error: null,
result: null,
start: null,
abort: null,
};
const reducer = (state, action) => {
switch (action.type) {
case 'init':
return initialState;
case 'ready':
return {
...state,
start: action.start,
abort: action.abort,
};
case 'start':
if (state.started) return state; // to bail out just in case
return {
...state,
started: true,
};
case 'result':
if (!state.pending) return state; // to bail out just in case
return {
...state,
pending: false,
result: action.result,
};
case 'error':
if (!state.pending) return state; // to bail out just in case
return {
...state,
pending: false,
error: action.error,
};
default:
throw new Error(`unexpected action type: ${action.type}`);
}
};
export const useAsyncTask = (func) => {
const [state, dispatch] = useReducer(reducer, initialState);
useEffect(() => {
let dispatchSafe = action => dispatch(action);
let abortController = null;
const start = async () => {
if (abortController) return;
abortController = new AbortController();
dispatchSafe({ type: 'start' });
try {
const result = await func(abortController);
dispatchSafe({ type: 'result', result });
} catch (e) {
dispatchSafe({ type: 'error', error: e });
}
};
const abort = () => {
if (abortController) {
abortController.abort();
}
};
dispatch({ type: 'ready', start, abort });
const cleanup = () => {
dispatchSafe = () => null; // avoid to dispatch after stopped
dispatch({ type: 'init' });
};
return cleanup;
}, [func]);
return state;
};
I really like that it has all the loading and error states returned but I cannot use this in an event handler.
Are there any options to use this in an event handler or how else can I get my loading and error states generically?
The README to the package I got the code from gives this example:
import React, { useState, useCallback } from 'react';
import {
useAsyncCombineSeq,
useAsyncRun,
useAsyncTaskDelay,
useAsyncTaskFetch,
} from 'react-hooks-async';
const Err = ({ error }) => <div>Error: {error.name} {error.message}</div>;
const Loading = ({ abort }) => <div>Loading...<button onClick={abort}>Abort</button></div>;
const GitHubSearch = ({ query }) => {
const url = `https://api.github.com/search/repositories?q=${query}`;
const delayTask = useAsyncTaskDelay(useCallback(() => 500, [query]));
const fetchTask = useAsyncTaskFetch(url);
const combinedTask = useAsyncCombineSeq(delayTask, fetchTask);
useAsyncRun(combinedTask);
if (delayTask.pending) return <div>Waiting...</div>;
if (fetchTask.error) return <Err error={fetchTask.error} />;
if (fetchTask.pending) return <Loading abort={fetchTask.abort} />;
if (!fetchTask.result) return <div>No result</div>;
return (
<ul>
{fetchTask.result.items.map(({ id, name, html_url }) => (
<li key={id}><a target="_blank" href={html_url}>{name}</a></li>
))}
</ul>
);
};
const App = () => {
const [query, setQuery] = useState('');
return (
<div>
Query:
<input value={query} onChange={e => setQuery(e.target.value)} />
{query && <GitHubSearch query={query} />}
</div>
);
};
This seems very inefficient, is there a better way?