how to prevent re-render react-redux - reactjs

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.

Related

How to detect that the state value of useSelector is changed?

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.

React Hooks: how to wait for the data to be fetched before rendering

I have fetch method in useEffect hook:
export const CardDetails = () => {
const [ card, getCardDetails ] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => getCardDetails(data))
}, [id])
return (
<DetailsRow data={card} />
)
}
But then inside DetailsRow component this data is not defined, which means that I render this component before data is fetched. How to solve it properly?
Just don't render it when the data is undefined:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data));
}, [id]);
if (card === undefined) {
return <>Still loading...</>;
}
return <DetailsRow data={card} />;
};
There are 3 ways to not render component if there aren't any data yet.
{data && <Component data={data} />}
Check if(!data) { return null } before render. This method will prevent All component render until there aren't any data.
Use some <Loading /> component and ternar operator inside JSX. In this case you will be able to render all another parts of component which are not needed data -> {data ? <Component data={data} /> : <Loading>}
If you want to display some default data for user instead of a loading spinner while waiting for server data. Here is a code of a react hook which can fetch data before redering.
import { useEffect, useState } from "react"
var receivedData: any = null
type Listener = (state: boolean, data: any) => void
export type Fetcher = () => Promise<any>
type TopFetch = [
loadingStatus: boolean,
data: any,
]
type AddListener = (cb: Listener) => number
type RemoveListener = (id: number) => void
interface ReturnFromTopFetch {
addListener: AddListener,
removeListener: RemoveListener
}
type StartTopFetch = (fetcher: Fetcher) => ReturnFromTopFetch
export const startTopFetch = function (fetcher: Fetcher) {
let receivedData: any = null
let listener: Listener[] = []
function addListener(cb: Listener): number {
if (receivedData) {
cb(false, receivedData)
return 0
}
else {
listener.push(cb)
console.log("listenre:", listener)
return listener.length - 1
}
}
function removeListener(id: number) {
console.log("before remove listener: ", id)
if (id && id >= 0 && id < listener.length) {
listener.splice(id, 1)
}
}
let res = fetcher()
if (typeof res.then === "undefined") {
receivedData = res
}
else {
fetcher().then(
(data: any) => {
receivedData = data
},
).finally(() => {
listener.forEach((cb) => cb(false, receivedData))
})
}
return { addListener, removeListener }
} as StartTopFetch
export const useTopFetch = (listener: ReturnFromTopFetch): TopFetch => {
const [loadingStatus, setLoadingStatus] = useState(true)
useEffect(() => {
const id = listener.addListener((v: boolean, data: any) => {
setLoadingStatus(v)
receivedData = data
})
console.log("add listener")
return () => listener.removeListener(id)
}, [listener])
return [loadingStatus, receivedData]
}
This is what myself needed and couldn't find some simple library so I took some time to code one. it works great and here is a demo:
import { startTopFetch, useTopFetch } from "./topFetch";
// a fakeFetch
const fakeFetch = async () => {
const p = new Promise<object>((resolve, reject) => {
setTimeout(() => {
resolve({ value: "Data from the server" })
}, 1000)
})
return p
}
//Usage: call startTopFetch before your component function and pass a callback function, callback function type: ()=>Promise<any>
const myTopFetch = startTopFetch(fakeFetch)
export const Demo = () => {
const defaultData = { value: "Default Data" }
//In your component , call useTopFetch and pass the return value from startTopFetch.
const [isloading, dataFromServer] = useTopFetch(myTopFetch)
return <>
{isloading ? (
<div>{defaultData.value}</div>
) : (
<div>{dataFromServer.value}</div>
)}
</>
}
Try this:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
if (!data) {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data))
}
}, [id, data]);
return (
<div>
{data && <DetailsRow data={card} />}
{!data && <p>loading...</p>}
</div>
);
};

Reactjs, class component to hooks

I'm still beginner with ReactJs. Actually I want to rewrite my class components to hook components but I have a problem with one part of my code. Anyone can help me with rewrite this component to hook?
This is my code:
class App extends Component {
state = {
selected: {},
data: data,
filtered: data
};
handleChange = data => {
if (data == null) {
this.setState({
filtered: this.state.data
});
} else {
this.setState({
selected: data,
filtered: this.state.data.filter(d => d.client_id === data.id)
});
}
};
returnClientNameFromID = id => options.find(o => o.id === id).name;
render() {
const {
state: { selected, data, filtered },
handleChange
} = this;
return ( <div>
...
Here's what you could do. With useState you always have to merge objects yourself setState((prevState) => {...prevState, ... })
const App = () => {
const [state, setState] = useState({
selected: {},
data: data,
filtered: data
})
const handleChange = data => {
if (data == null) {
setState((prevState) => {
...prevState,
filtered: this.state.data
});
} else {
setState((prevState) => {
...prevState,
selected: data,
filtered: prevState.data.filter(d => d.client_id === data.id)
});
}
};
const returnClientNameFromID = id => options.find(o => o.id === id).name;
const { selected, data, filtered } = state
return() (
<div> ... </div>
)
}

getting callstack with react hooks and infinite scroll?

i have already tried useMemo and useEffect, but i can't seem to figure out why my code don't work:
const App: React.FC = () => {
const dispatch = useDispatch();
const [page, setPage] = useState(1);
const { userIds, users, totalUsers } = useSelector(
({ users }: RootState) => users
);
const renderUsers = useMemo(() => {
return userIds.map(userId => (
<div key={users[userId].first_name}>{users[userId].first_name}</div>
));
}, [userIds, users]);
const hasMore = useMemo(() => {
return userIds.map(userId => userId).length < totalUsers;
}, [userIds, totalUsers]);
const fetchUsers = useCallback(
async (page: number) => {
dispatch({
type: FETCH_USERS_REQUEST,
payload: { page }
});
try {
const { data, ...result } = await api.fetchUsers(page);
const user = new schema.Entity('users');
const {
entities,
result: { users: userIds }
} = normalize({ users: data }, { users: [user] });
dispatch({
type: FETCH_USERS_SUCCESS,
payload: {
...result,
users: entities.users,
userIds
}
});
} catch (error) {
dispatch({ type: FETCH_USERS_FAILURE, payload: { error } });
}
},
[dispatch]
);
useEffect(() => {
fetchUsers(1);
}, [fetchUsers]);
let scrollParentRef: HTMLDivElement | null = null;
return (
<div className="vh-100 vw-100">
<Header />
<div
className="container overflow-auto"
ref={div => {
scrollParentRef = div;
}}
>
<InfiniteScroll
pageStart={0}
loadMore={async page => await fetchUsers(page)}
hasMore={hasMore}
loader={
<div className="loader" key={0}>
Loading ...
</div>
}
useWindow={false}
getScrollParent={() => scrollParentRef}
>
{renderUsers}
</InfiniteScroll>
</div>
</div>
);
};
I am using infinite scroll and the error i got is call stack, please help me fix this?

async task with useReducer that is not a hook in react

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?

Resources