I need to rerender functional component - reactjs

I have a component
const MovieDetail = (props) => {
//const [state, setstate] = useState(initialState)
const { id } = useParams();
const movie = useSelector(getMovie)
const movies = useSelector(getAllMovies)
const dispatch = useDispatch();
useEffect(() => {
fetchMovieById(id)
.then((response) => dispatch(addMovie(response)))
}, [movies])
const onSubmit = (fromData) => {
//here
dispatch(addComments({fromData, id, movies}));
console.log(fromData);
}
return (
<div className='container'>
<br/>
{
<div className='detail'>
<div>
<h2>{movie.title}</h2>
<div>
<img src={movie.img} alt={movie.title}/>
</div>
</div>
<div className='description'>
<div>
{movie.details}
</div>
<div className='comments'>
<h5>Stay your comment here</h5>
<CommentReduxForm onSubmit={onSubmit}/>
</div>
<div className='comments'>
{movie.comments ? movie.comments : <div>no comments yet</div>}
</div>
<div className='ratio'>
<strong>Ratio: {movie.ratio}</strong>
</div>
</div>
</div>
}
</div>
)
}
export default MovieDetail
I'm using redux toolkit, I'm trying to add comment to movie. I come on the site and select the movie, in the form I put my comment, then I click bottun to add comment, then I go to devtools in my browser and I see My comment in the state, but on the page, I see "there are not comments", so I have updated state, but not updated page, and i think I need to rerender my component, or maybe I need another technic?
Also there is my slice with actions and reducers, help me please if anybody can
const initialState = {
movies: [],
movie: {}
}
const movieSlice = createSlice({
name: "movies",
initialState,
reducers: {
addMovies: (state, {payload}) => {
state.movies = payload;
},
addMovie: (state, action) => {
state.movie = action.payload[0];
},
addComments: (state, action) => {
debugger
let author = action.payload.fromData.yourName;
let comment = action.payload.fromData.yourComment;
let movieId = action.payload.id;
let moviesArr = action.payload.movies;
let obj = moviesArr.find((item) => item.id == movieId);
let newObj = {...obj, comments: comment}
const newArr = moviesArr.map(o => {
if (o.id === newObj.id) {
return newObj;
}
return o;
})
state.movies = newArr;
}
},
});
export const {addMovies, addMovie, addComments} = movieSlice.actions;
export const getAllMovies = (state) => state.movies.movies;
export const getMovie = (state) => state.movies.movie;
export default movieSlice.reducer;

In Redux, reducers are not allowed to mutate the original / current state values
You can see these rules in reducer rules
So you should return an updated state instead of changing the oringials
const initialState = {
movies: [],
movie: {}
}
const movieSlice = createSlice({
name: "movies",
initialState,
reducers: {
addMovies: (state, {payload}) => {
return {movies:payload,...state}
},
addMovie: (state, action) => {
return {movie:action.payload[0],...state}
},
addComments: (state, action) => {
let author = action.payload.fromData.yourName;
let comment = action.payload.fromData.yourComment;
let movieId = action.payload.id;
let moviesArr = action.payload.movies;
let obj = moviesArr.find((item) => item.id == movieId);
let newObj = {...obj, comments: comment}
const newArr = moviesArr.map(o => {
if (o.id === newObj.id) {
return newObj;
}
return o;
})
return {movies:newArr,...state}
}
},
});
export const {addMovies, addMovie, addComments} = movieSlice.actions;
export const getAllMovies = (state) => state.movies.movies;
export const getMovie = (state) => state.movies.movie;
export default movieSlice.reducer;

Related

Infinite Loop using React, Redux and Firebase

I have a collection of profile documents in firebase and I want to render them in the profiles page, however after I have updated the userProfiles state and use useDispatch to store the state in the slice, I get an infinite loop when rendering the profile page.
I have tried putting the dispatch() into a useEffect, not in a useEffect and inside the querySnapshot promise but I'm still getting an infinite loop wherever I put it.
Any feedback is appreciated, thank you.
\\ profiles.js
export const Profiles = () => {
const [userProfiles, setUserProfiles] = useState([]);
const dispatch = useDispatch();
const navigate = useNavigate();
const user = useSelector(selectUser);
db.collection("customers")
.doc(user.info.uid)
.collection("profiles")
.get()
.then((querySnapshot) => {
const documents = querySnapshot.docs.map((doc) => doc.data());
setUserProfiles(documents);
});
useEffect(() => {
dispatch(profiles(userProfiles));
}, []);
console.log({ userProfiles });
return (
<div className="profile_container">
<h1 className="profile_title">Who's Watching?</h1>
<div className="profile_row">
{userProfiles.map((profile) => {
return (
<div className="profile_individualProfile">
<img
src="https://occ-0-300-1167.1.nflxso.net/dnm/api/v6/K6hjPJd6cR6FpVELC5Pd6ovHRSk/AAAABY5cwIbM7shRfcXmfQg98cqMqiZZ8sReZnj4y_keCAHeXmG_SoqLD8SXYistPtesdqIjcsGE-tHO8RR92n7NyxZpqcFS80YfbRFz.png?r=229"
alt="profile"
/>
<p>{profile.name}</p>
</div>
);
})}
<div
onClick={() => navigate("/add-profile")}
className="profile_addProfile_container"
>
<img
src="https://img.icons8.com/ios-glyphs/30/FFFFFF/plus--v1.png"
alt="add profile"
/>
<h2>Add Profile</h2>
</div>
</div>
</div>
);
};
\\ userSlice.js
export const userSlice = createSlice({
name: "user",
initialState: {
user: {
info: null,
profiles: [],
},
},
reducers: {
login: (state, action) => {
state.user.info = action.payload;
},
logout: (state) => {
state.user.info = null;
},
profiles: (state, action) => {
state.user.profiles.push(action.payload);
},
},
});
In the current implementation, when your page is rendered, db.collections runs and you set state setUserProfiles(documents) which renders your app and again db.collections runs. to prevent this you should run db.collections in useEffect.
// fetch users only when your app renders
useEffect(() => {
db.collection("customers")
.doc(user.info.uid)
.collection("profiles")
.get()
.then((querySnapshot) => {
const documents = querySnapshot.docs.map((doc) => doc.data());
setUserProfiles(documents);
});
}, []);
have another useEffect
useEffect(() => {
dispatch(profiles(userProfiles));
}, [userProfiles]);
this will NOT work neither. setUserProfiles will be causing issue. Because when app renders, you fetch data, you set the state, change the userProfiles, this will rerender app again.
The problem with your code is you do not need setUserProfiles. instead in db.collections() when you get the documents, you dispatch the documents and then access the profiles from redux with useSelector
// fetch users only when your app renders
useEffect(() => {
db.collection("customers")
.doc(user.info.uid)
.collection("profiles")
.get()
.then((querySnapshot) => {
const documents = querySnapshot.docs.map((doc) => doc.data());
// setUserProfiles(documents); You do not need this
dispatch(profiles(userProfiles))
});
}, []);
Now use useSelector to reach the state in redux
// assuming reducers name is "users"
const usersState = useSelector((state) => state.users);
now when you use map guard your app
// make sure you use the correct data
// you migh need to destructure
{usersState && usersState.map((profile) => {
For anyone that runs into this issue you may find this useful. Following from yilmaz's helpful answer, I had to update the Profiles.js and userSlice.js as follows...
// Profiles.js
export const Profiles = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const usersState = useSelector(profiles);
useEffect(() => {
db.collection("customers")
.doc(usersState.payload.user.user.info.uid)
.collection("profiles")
.get()
.then((querySnapshot) => {
const documents = querySnapshot.docs.map((doc) => doc.data());
!usersState.payload.user.user.profiles.includes((arr) =>
documents.every(arr)
) && dispatch(profiles(documents));
});
}, []);
return (
<div className="profile_container">
<h1 className="profile_title">Who's Watching?</h1>
<div className="profile_row">
{usersState.payload.user.user.profiles.map((profile) => {
console.log(profile);
return (
<div className="profile_individualProfile">
<img
src="https://occ-0-300-1167.1.nflxso.net/dnm/api/v6/K6hjPJd6cR6FpVELC5Pd6ovHRSk/AAAABY5cwIbM7shRfcXmfQg98cqMqiZZ8sReZnj4y_keCAHeXmG_SoqLD8SXYistPtesdqIjcsGE-tHO8RR92n7NyxZpqcFS80YfbRFz.png?r=229"
alt="profile"
/>
<p>{profile.name}</p>
</div>
);
})}
<div
onClick={() => navigate("/add-profile")}
className="profile_addProfile_container"
>
<img
src="https://img.icons8.com/ios-glyphs/30/FFFFFF/plus--v1.png"
alt="add profile"
/>
<h2>Add Profile</h2>
</div>
</div>
</div>
);
};
// userSlice.js
export const userSlice = createSlice({
name: "user",
initialState: {
user: {
info: null,
profiles: [],
},
},
reducers: {
login: (state, action) => {
state.user.info = action.payload;
},
logout: (state) => {
state.user.info = null;
},
profiles: (state, action) => {
state.user.profiles.length = 0;
state.user.profiles.push(...action.payload);
},
},
});

useEffect goes to cyclic dependency with Redux Toolkit

Tryed to render component using useEffect with "product" dependency but it goes cyclic dependency.
Tryed to use prev state but it doesn't help.
React don't gives any error, but useEffect send request every second.
Component:
export const ProductPage = (props: any) => {
const { product, isLoading, error } = useTypedSelector(state => state.Product)
const dispatch = useTypedDispatch()
const { id } = useParams()
const prevProd = usePrevious(product)
useEffect(() => {
if (prevProd !== product){dispatch(fetchProduct(Number(id)))}
}, [product])
return (
<div>
<div>{product.name}</div>
<div>{product.id}</div>
<div>{product.price}</div>
</div>
)
}
Async Thunk:
export const fetchProduct = createAsyncThunk(
'product/fetch',
async (id: number, thunkApi) => {
try {
const response = await productService.fetch(id)
return response.data
} catch (error: any) {
return thunkApi.rejectWithValue(error.message)
}
}
)
Slice:
export const ProductSlice = createSlice({
name: 'product',
initialState,
reducers: {},
extraReducers: {
[fetchProduct.fulfilled.type]: (state, action: PayloadAction<IProductData>) => {
state.error = ''
state.isLoading = false
state.product = action.payload
},
[fetchProduct.pending.type]: (state) => {
state.isLoading = true
},
[fetchProduct.rejected.type]: (state, action: PayloadAction<string>) => {
state.isLoading = false
state.error = action.payload
},
}
})
Explain please, why this problem occured and how resolve this?

Redux wont remove item from state array by ID

I dont understant what is wrong with this code, im passing in ID and filtering out state, but it just wont remove, cant figure it out, would love some help.
slice:
import { createSlice } from "#reduxjs/toolkit";
const movieSlice = createSlice({
name: "movie",
initialState: { favoriteMovies: [] },
reducers: {
addMovie: (state, action) => {
state.favoriteMovies = [...state.favoriteMovies, action.payload];
},
removeMovie: (state, action) => {
state.favoriteMovies = state.favoriteMovies.filter(
(movie) => movie.imdbID !== action.payload
);
},
},
});
export const { addMovie, removeMovie } = movieSlice.actions;
export const selectMovie = (state) => state.movie.favoriteMovies;
export default movieSlice.reducer;
dispatching:
const MovieDetail = ({ movie }) => {
const [isFavorite, setIsFavorite] = useState(false);
const dispatch = useDispatch();
const imdbID = movie.imdbID;
const handleAddFavorite = () => {
dispatch(addMovie({ movie }));
setIsFavorite(true);
};
const handleRemoveFavorite = () => {
dispatch(removeMovie({ imdbID }));
console.log(imdbID);
setIsFavorite(false);
};
it does nothing when should remove, and then add it again. The ids i pass in are correct.
The way you are dispatching with
dispatch(removeMovie({ imdbID }));
imdbID will end up as action.payload.imdbID.
So you need to either access that, or dispatch like
dispatch(removeMovie(imdbID));

Using a callback with useReducer / Redux reducers

I've been looking for a solution to "promisify" useReducer do something once I'm sure that the state has been changed as per my dispatched action. I found some promising stuff, such as this feature request and a few solutions similar to this one that's based on combining useReducer with useEffect. So instead of using a promise, I've tried to use a callback instead and I want to note here that this implementation works. But I'm unsure if there are any drawbacks to this.
**Note, the use case here isn't to call a function per every time the state changes, but rather the option to do something when the reducer finishes processing an action.
As per Redux rules, my reducer does not mutate state.
const emptyState: IState = {
str: '',
obj: {
propA: 0,
propB: 0,
}
}
interface ReducerActions {
type: 'changeStr' | 'changeObj';
callback?: (newState: IState) => any;
}
const reducer = (state: IState, action: ReducerActions): IState => {
let newState = {...state};
switch(action.type) {
case 'changeStr':
newState.str = action.newStr;
break;
case 'changeObj':
newState.obj = action.newObj;
break;
if (action.callback) {
action.callback(newState);
}
return newState;
}
I did notice that this works in reverse of the traditional flow, where the callback or promise is executed after the state has changed, but should it matter when the callback is called using the value of the new state anyways?
And, are there any drawbacks or side-effects of using this method (whether here or in a Redux implementation)?
the action changeStr replaces the string of state.str with a new string, using 2 buttons, one makes it longer, the other shorter. If I use useEffect, I can of course check the new value of the string and get the length, but I would not be able to get the length of the previous string without storing the previous value. If I pass a callback to the action implemented in the buttons, I know which button makes it longer and which one makes it shorter.
Still not sure what your needs are but if you need the previous and current value to perform some logic you can write a custom hook:
const { useReducer, useState, useRef } = React;
const init = { value: 'A' };
const TOGGLE = 'TOGGLE';
const toggle = () => ({ type: TOGGLE });
const reducer = (state, { type }) => {
//toggle state.value between A and B
if (type === TOGGLE) {
return { value: state.value === 'A' ? 'B' : 'A' };
}
return state;
};
const selectValue = (state) => state.value;
const NONE = {};
//custom hook to detect changes between renders
const useChange = (value, callback) => {
const ref = useRef(NONE);
if (ref.current !== value) {
if (ref.current !== NONE) {
callback(ref.current, value);
}
ref.current = value;
}
};
const App = () => {
const [state, dispatch] = useReducer(reducer, init);
const [message, setMessage] = useState('');
const value = selectValue(state);
useChange(value, (pref, current) =>
setMessage(`value changed from ${pref} to ${current}`)
);
return (
<div>
<button onClick={() => dispatch(toggle())}>
toggle
</button>
<div>{value}</div>
<div>{message}</div>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
UPDATE
Example of using thunk with useReducer:
const { useReducer, useRef } = React;
const init = { value: 'A' };
const TOGGLE = 'TOGGLE';
const thunkToggle = () => (dispatch, getState) => {
const value = getState().value;
//you can do async dispatch
setTimeout(() => {
dispatch({ type: TOGGLE });
console.log(
`action dispatched value was ${value} is now ${
getState().value
}`
);
}, 10);
console.log(`value is now (nothing dispatched) ${value}`);
};
const reducer = (state, { type }) => {
console.log(`in reducer action type: ${type}`);
//toggle state.value between A and B
if (type === TOGGLE) {
return { value: state.value === 'A' ? 'B' : 'A' };
}
return state;
};
const SET_STATE = Date.now();
//custom hook to detect changes between renders
const useThunkReducer = (reducer, initialState) => {
const state = useRef(initialState);
const thunkReducer = (state, action) => {
if (action.type === SET_STATE) {
return action.payload;
}
return reducer(state, action);
};
const [rState, dispatch] = useReducer(thunkReducer, init);
const thunkDispatch = (action) => {
if (typeof action === 'function') {
return action(thunkDispatch, () => state.current);
}
state.current = thunkReducer(state.current, action);
dispatch({ type: SET_STATE, payload: state.current });
};
return [rState, thunkDispatch];
};
const App = () => {
const [state, dispatch] = useThunkReducer(reducer, init);
return (
<div>
<button onClick={() => dispatch(thunkToggle())}>
toggle
</button>
<pre>{JSON.stringify(state, undefined, 2)}</pre>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
For completion; here is an example using middleware so you can add several middleware functions and not only thunk:
const { useRef, useState } = React;
const compose = (...fns) =>
fns.reduce((result, fn) => (...args) =>
fn(result(...args))
);
const mw = () => (next) => (action) => next(action);
const createMiddleware = (...middlewareFunctions) => (
store
) =>
compose(
...middlewareFunctions
.concat(mw)
.reverse()
.map((fn) => fn(store))
);
const useMiddlewareReducer = (
reducer,
initialState,
middleware = () => (b) => (c) => b(c)
) => {
const stateContainer = useRef(initialState);
const [state, setState] = useState(initialState);
const dispatch = (action) => {
const next = (action) => {
stateContainer.current = reducer(
stateContainer.current,
action
);
return setState(stateContainer.current);
};
const store = {
dispatch,
getState: () => stateContainer.current,
};
return middleware(store)(next)(action);
};
return [state, dispatch];
};
//middleware
const thunkMiddleWare = ({ getState, dispatch }) => (
next
) => (action) =>
typeof action === 'function'
? action(dispatch, getState)
: next(action);
const logMiddleware = ({ getState }) => (next) => (
action
) => {
console.log('in log middleware', action, getState());
Promise.resolve().then(() =>
console.log('after action:', action.type, getState())
);
return next(action);
};
const init = { value: 'A' };
const TOGGLE = 'TOGGLE';
const thunkToggle = () => (dispatch) => {
setTimeout(() => {
dispatch({ type: TOGGLE });
}, 500);
};
const reducer = (state, { type }) => {
console.log(`in reducer action type: ${type}`);
//toggle state.value between A and B
if (type === TOGGLE) {
return { value: state.value === 'A' ? 'B' : 'A' };
}
return state;
};
const middleware = createMiddleware(
thunkMiddleWare,
logMiddleware
);
const App = () => {
const [state, dispatch] = useMiddlewareReducer(
reducer,
init,
middleware
);
return (
<div>
<button onClick={() => dispatch(thunkToggle())}>
toggle
</button>
<pre>{JSON.stringify(state, undefined, 2)}</pre>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

how to prevent re-render react-redux

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.

Resources