React Hooks & redux cause unnecesary rendering and some unexpected effects - reactjs

Expected behavior - I load an photo object and render it in my component.
With the code below I do achieve the result, but with some unexpected effects which in the end make this component not useful - see further description in two parts.
I really want to dive in the cause of all of it and to understand how can I prevent such behavior in my app.
I did try some suggestions from others peoples similar problems, but nothing did helped. I won't list here the things I've tried, because, obviously, everytime I tried - I did something wrong since it didn't help me.
I will be grateful for any ideas and suggestions - I miss something and can't understand what is it.
Part 1 of the problem.
While loading this component for the first time and/or refreshing it - I get multiple rerenders. From the Redux DevTools I can observ that the actions fire for two times, console-logging any received from the photo value shows that this value appears in the console 6 times (first 3 times - with initial state from the redux-store, after - with the expected fetched from the photo object value).
Part 2 of the problem.
When I open the next photo (the same component, just passing different match.params.id) - the component starting to rerender apparently for random times. It might take some seconds to complete this rerender loop, so it rerenders sometimes for dozens, sometimes for more then a 100 time, but always in the end is rendering the needed info.
Analyzing the logs I saw that the the values of fetched now photo are just switching in the loop with the values of the photo fetched before. The looping stops with the correct values. And where from the previos values are coming - I can't figure out, because before fetching a new photo object I clear all the data of the previous in the redux state.
Component:
//IMPORTS
const Photo = ({ getPhotoById, photo, loading, match }) => {
const [photoData, setPhotoData] = useState({
photoID: match.params.id,
imgUrl: '',
photoFileName: '',
title: '',
description: '',
albumID: '',
albumName: '',
categoryID: '',
categoryName: '',
categoryID2: '',
categoryName2: '',
categoryID3: '',
categoryName3: '',
locationID: '',
locationName: '',
contributorID: '',
contributorName: '',
contributorWeb: '',
source: '',
sourceWeb: '',
author: '',
periodID: '',
periodName: '',
license: ''
});
const {
photoID,
imgUrl,
photoFileName,
title,
description,
albumID,
albumName,
categoryID,
categoryName,
categoryID2,
categoryName2,
categoryID3,
categoryName3,
locationID,
locationName,
contributorID,
contributorName,
source,
sourceWeb,
author,
periodID,
periodName,
license
} = photoData;
useEffect(() => {
getPhotoById(photoID);
}, [getPhotoById, photoID]);
useEffect(() => {
if (loading === false) {
const {
photoID,
imgUrl,
photoFileName,
title,
description,
albumID,
albumName,
categoryID,
categoryName,
categoryID2,
categoryName2,
categoryID3,
categoryName3,
locationID,
locationName,
contributorID,
contributorName,
source,
sourceWeb,
author,
periodID,
periodName,
license
} = photo;
setPhotoData({
photoID,
imgUrl,
photoFileName,
title,
description,
albumID,
albumName,
categoryID,
categoryName,
categoryID2,
categoryName2,
categoryID3,
categoryName3,
locationID,
locationName,
contributorID,
contributorName,
source,
sourceWeb,
author,
periodID,
periodName,
license
});
}
}, [loading]);
useEffect(() => {
if (!loading) {
initOpenseadragon();
}
}, [loading]);
console.log(photoFileName, 'photoFileName');
const initOpenseadragon = () => {
OpenSeadragon({
id: 'viewer',
tileSources: `/uploads/tiles/${photoFileName}.dzi`,
prefixUrl: '/images/osd/',
showZoomControl: true,
showHomeControl: true,
showFullPageControl: true,
showRotationControl: true
});
};
return !photo && !loading ? (
<NotFound />
) : (
<Fragment>
SOME JSX
</Fragment>
);
};
Photo.propTypes = {
getPhotoById: PropTypes.func.isRequired,
// photo: PropTypes.object.isRequired,
loading: PropTypes.bool.isRequired
};
const mapStateToProps = state => {
return {
photo: state.photo.photo,
loading: state.photo.loading
};
};
export default connect(
mapStateToProps,
{ getPhotoById }
)(Photo);
ACTION:
export const getPhotoById = photo_id => async dispatch => {
try {
dispatch({ type: CLEAR_PHOTO });
dispatch({ type: LOAD_PHOTO });
const res = await axios.get(`/api/photo/${photo_id}`);
dispatch({
type: GET_PHOTO,
payload: res.data
});
} catch (err) {
dispatch({
type: PHOTOS_ERROR,
payload: { msg: err.response.statusText, status: err.response.status }
});
}
};
REDUCER
const initialState = {
photo: null,
photos: [],
loading: true,
error: {}
};
const photo = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_PHOTO:
return {
...state,
photo: payload,
loading: false
};
case LOAD_PHOTO:
return {
...state,
loading: true
};
case CLEAR_PHOTO:
return {
...state,
photo: null,
loading: false
};
case PHOTOS_ERROR:
return {
...state,
error: payload,
loading: false
};
default:
return state;
}
};
export default photo;

Your problem is that you are adding getPhotoById as a dependency of your hook see this article about the dependency array.
If you want to prevent the re-render you can do the following:
const ref = useRef();
getPhotoByIdRef.current = getPhotoById
useEffect(() => {
getPhotoByIdRef(match.params.id)
}, [getPhotoByIdRef, match.params.id]);

Related

React native component rerender when using context api

I'm currently learning React Native. I have an aplication that is used to track expenses and I'm setting some dummy data as initial state. The app works fine, I can add new expenses, edit and delete them. Also, I have a component that is showing recent expenses for the last 7 days only.
Today I noticed that my recent list was empty, because all of the expenses were older than the last 7 days, so I changed the dates of a few items so I would have some data on initial load.
After saving the changes, I noticed that the app refreshed, but the state didn't, I still had an empty list. Only when I forced the reload of the app, the state was updated.
So my question is, is this intended? Should the component rerender after I manually change the initial state or not? I couldn't find any similar issues and also I couldn't find anything in the docs.
Here's my context code:
import { createContext, useReducer } from 'react';
const DUMMY_EXPENSES = [
{
id: 'e1',
description: 'A pair of shoes',
amount: 59.99,
date: new Date('2021-12-19'),
},
{
id: 'e2',
description: 'A pair of trousers',
amount: 89.29,
date: new Date('2022-01-02'),
},
{
id: 'e3',
description: 'Bananas',
amount: 19.99,
date: new Date('2023-02-06'),
},
{
id: 'e4',
description: 'Book',
amount: 69.69,
date: new Date('2023-02-05'),
},
];
export const ExpensesContext = createContext({
expenses: [],
addExpense: ({ description, amount, date }) => {},
deleteExpense: (id) => {},
updateExpense: (id, { description, amount, date }) => {},
});
function expensesReducer(state, action) {
switch (action.type) {
case 'ADD':
const id = new Date().toString() + Math.random().toString();
return [{ ...action.payload, id: id }, ...state];
case 'UPDATE':
const expenseIndex = state.findIndex((e) => e.id === action.payload.id);
const expenses = [...state];
expenses[expenseIndex] = { id: action.payload.id, ...action.payload.data };
return expenses;
case 'DELETE':
const expenseArray = [...state];
return expenseArray.filter((e) => e.id !== action.payload);
default:
return state;
}
}
export default function ExpensesContextProvider({ children }) {
const [expenses, dispatch] = useReducer(expensesReducer, DUMMY_EXPENSES);
function addExpense(expenseData) {
dispatch({ type: 'ADD', payload: expenseData });
}
function deleteExpense(id) {
dispatch({ type: 'DELETE', payload: id });
}
function updateExpense(id, expenseData) {
dispatch({ type: 'UPDATE', payload: { id: id, data: expenseData } });
}
const value = {
expenses: expenses,
addExpense: addExpense,
updateExpense: updateExpense,
deleteExpense: deleteExpense,
};
return <ExpensesContext.Provider value={value}>{children}</ExpensesContext.Provider>;
}

add inputs dynamically react redux

hello I will ask this question again because I still can’t find a answer
I try to create a form similar to google form with react and redux
each question is represented by: QuestionCard
contains a title and can have several types (short question, long question, choice ,multiple, single choice ...)
I manage to add cardQuestion and delete them
my problem is that when I select single choice I want to give the user the possibility to add the number of choices he wants but it does not work
this is my reducer
const initialState = {
formId: '',
form: {
title: '',
description: ''
},
basicForm: {
fullName: '',
age: '',
saved: false
},
cardQuestion: [{
title: '',
choix: [],
type: '',
}],
error: "",
loading: false,
}
const addRequestReducer = (state = initialState, action) => {
switch (action.type) {
case 'SET_STATE':
return { ...state, ...action.payload }
default:
return state
}
}
i use a global state
and this how i try to add choices
1- i handle change of inputs (each choice) like this :
const { cardQuestion } = useSelector((state) => state.addRequest)
const choice = cardQuestion.map(e => e.choix)
const onChangeInput = (i, key, value) => {
console.log(value)
dispatch({
type: 'SET_STATE',
payload: {
cardQuestion: cardQuestion.map((elem, index) => {
console.log(i)
console.log(i === index)
if (i !== index) return elem
return { ...elem, [`${key}`]: value }
})
}
})
}
2- use the ChangeInptut like this
<div >{choice.map((elem, i) => <div key={i}>
<Input value={elem.choix} onChange={(e) => onChangeInput(i, 'choix', e.target.value)} />
</div>
3- button to add new input (choice)
<Button onClick={() => dispatch({
type: 'SET_STATE',
payload: {
cardQuestion: [...cardQuestion, { choix: '' }]
}
})} >Add</Button>
</div>
but it give me results like this :
and when i add a choice in question card1 it is also added in questioncard2
how can i resolve this , any help will be appreciated
This
cardQuestion: [...cardQuestion, { choix: '' }]
adds a new card without a title or type to the list of cards. If I understand you correctly, you want to add a new choice to an existing card, which will look something like
const newCards = [...cardQuestion]
newCards[currentCardPosition] = {...newCards[currentCardPosition]}
newCards[currentCardPosition].choix = [...newCards[currentCardPosition].choix, '' ]
but I don't see where currentCardPosition will come from

Rematch/Reducers- Error writing the test cases

I am trying to write unit test cases for my reducers.js using React Testing Library.I am getting some error which i am not able to figure out. Can someone help me understand where i am going wrong?
reducers.js-
const INITIAL_STATE = {
userData: {},
};
const setUserData = (state, { key, value }) => ({ // {key: value}
...state,
userData: {
...state.userData,
[key]: value,
},
});
reducers.test.js
import reducersDefault from './reducers';
const {
setUserData,
} = reducersDefault.reducers;
describe('reducers', () => {
it('setUserData', () => expect(setUserData({}, { key: { name: 'test' } })).toEqual({
userData: { userData: { key: { name: 'test' } } },
}));
});
With the above code, i am getting the below error-
Expected value to equal:
{"userData": {"userData": {"key": {"name": "test"}}}}
Received:
{"userData": {"undefined": undefined}}
Trying to figure out what i am doing wrong here. Any help is much appreciated.
You test fails because your function doesn't work properly. You cannot destructure an object to key/value - what you are doing currently extracts the values of key and value properties of the object you are passing there.
Here's a better approach:
const setUserData = (state, data) => ({
...state,
userData: {
...state.userData,
..data, // put every property inside data to userData
},
});
LE: After reading your comment I realised you are calling your function wrong in the test:
expect(setUserData({}, { key: 'name', value: 'test' })).toEqual({
userData: { name: 'test' }
}));
This should work as you expect (without changing setUserData).

How can I see state within a function? using hooks

I'm trying to update the uploadFiles state inside my updateFile function, when reloading the file, I'm rewriting this component in hooks, but inside the function the state is given as empty.
const [uploadedFiles, setUploadedFiles] = useState({
slides: [],
material: [],
});
const updateFile = useCallback(
(id, data) => {
const value = uploadedFiles.slides.map(uploadedFile => {
return id === uploadedFile.id
? { ...uploadedFile, ...data }
: uploadedFile;
});
console.log('value', value);
console.log('uploadedFilesOnFunction', uploadedFiles);
},
[uploadedFiles]
);
function processUpload(upFile, type) {
const data = new FormData();
data.append('file', upFile.file, upFile.name);
api
.post('dropbox', data, {
onUploadProgress: e => {
const progress = parseInt(Math.round((e.loaded * 100) / e.total), 10);
updateFile(upFile.id, {
progress,
});
},
})
.then(response => {
updateFile(upFile.id, {
uploaded: true,
id: response.data.id,
url: response.data.url,
type,
});
})
.catch(response => {
updateFile(upFile.id, {
error: true,
});
});
}
function handleUpload(files, type) {
const uploaded = files.map(file => ({
file,
id: uniqueId(),
name: file.name,
readableSize: filesize(file.size),
preview: URL.createObjectURL(file),
progress: 0,
uploaded: false,
error: false,
url: null,
type,
}));
setUploadedFiles({
slides: uploadedFiles.slides.concat(uploaded),
});
uploaded.forEach(e => processUpload(e, type));
}
console.log('slides', uploadedFiles);
I expected the state values to be viewed by the function. For me to manipulate and set the state.
There might be other issues, but one thing I've noticed is:
const [uploadedFiles, setUploadedFiles] = useState({
slides: [],
material: [],
});
// A setState CALL FROM THE useState HOOK REPLACES THE STATE WITH THE NEW VALUE
setUploadedFiles({
slides: uploadedFiles.slides.concat(uploaded),
});
From: https://reactjs.org/docs/hooks-state.html
State variables can hold objects and arrays just fine, so you can still group related data together. However, unlike this.setState in a class, updating a state variable always replaces it instead of merging it.
The setState from the useState hook doesn't merge the state. Because it can hold any type of value, not only objects, like we used to do with classes.
From your code you can see that you're erasing some property from state when you're updating like that.
Instead, you should use the functional form of the setState and access the current state prevState, like:
setUploadedFiles((prevState) => {
return({
...prevState,
slides: uploadedFiles.slides.concat(uploaded)
});
});
The updated updateFiles function:
const updateFile = (id, data) => {
setUploadedFiles(prevState => {
const newSlide = prevState.slides.map(slide => {
return id === slide.id ? { ...slide, ...data } : slide;
});
return {
...prevState,
slides: newSlide,
};
});
};

Filter products depend on another ACTION in React-native Redux

I have an app which get all categories and products from the server with Redux ACTIONS. I need to filter products with a category Id. after load data action is complete, i call another action to filter products but i'm a little bit confused.
There is codes of few parts of the app:
ProductsActions:
export const GET_INITIAL_PRODUCTS_DATA = "GET_INITIAL_PRODUCTS_DATA";
export const GET_INITIAL_PRODUCTS_DATA_RESULT = "GET_INITIAL_PRODUCTS_DATA_RESULT";
export const GET_INITIAL_PRODUCTS_DATA_ERROR = "GET_INITIAL_PRODUCTS_DATA_ERROR";
export const FILTER_PRODUCTS_BY_CATEGORY_ID = "FILTER_PRODUCTS_BY_CATEGORY_ID";
export const getInitialProductsData = () => ({
type: GET_INITIAL_PRODUCTS_DATA
});
export const filterProductsByCategoryId = categoryId => ({
type: FILTER_PRODUCTS_BY_CATEGORY_ID,
categoryId
});
ProductsReducers:
import {
GET_INITIAL_PRODUCTS_DATA,
GET_INITIAL_PRODUCTS_DATA_RESULT,
GET_INITIAL_PRODUCTS_DATA_ERROR,
FILTER_PRODUCTS_BY_CATEGORY_ID
} from "../actions/products";
const initialState = {
isFetching: false,
data: {},
error: null
};
const filterProductsByCategoryId = (state, action) => {
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case GET_INITIAL_PRODUCTS_DATA:
return {
...state,
isFetching: true
};
case GET_INITIAL_PRODUCTS_DATA_RESULT:
return {
...state,
isFetching: false,
data: action.result
};
case GET_INITIAL_PRODUCTS_DATA_ERROR:
return {
...state,
isFetching: false,
error: action.error
};
case FILTER_PRODUCTS_BY_CATEGORY_ID:
return {
...state,
data: filterProductsByCategoryId(state, action.categoryId)
};
default:
return state;
}
};
export default reducer;
And there is my code to call filter action:
filterProducts = (title = "A") => {
const _categories = Object.values(this.props.categories);
const selectedCategory = _categories.find(
category => category.title === title
);
this.props.dispatch(filterProductsByCategoryId(selectedCategory.id));
My questions is:
A) Is there is a way to filter my data and display them in UI and refresh them without using ACTIONS way??
B) If A's answer is No!, How can i get my state.data and filter them in FILTER_PRODUCTS_BY_CATEGORY_ID?
Thanks.
You can use the Array.prototype.filter() to return filtered result.
keep in mind that this will return an array and not a single value, which is a good thing if you are using this filter within your reducer. because your reducer's shape is an array and not an object.
Running example:
const myData = [{
name: 'some name',
id: 1
}, {
name: 'some name2',
id: 2
}, {
name: 'some name3',
id: 3
}, {
name: 'some name4',
id: 4
}]
const filterProductsByCategoryId = (state, action) => {
return state.filter(c => c.id === action.categoryId);
};
const result = filterProductsByCategoryId(myData, {categoryId: 2});
console.log(result);
I think it is more appropriate to create a selector for a singular product that will handle this kind of action, this way you will be able to return an object instead of an array with one product in it.
Not to mention the benefits of using reselect to do some memoizations.
For this task you can use the Array.prototype.find():
const myData = [{
name: 'some name',
id: 1
}, {
name: 'some name2',
id: 2
}, {
name: 'some name3',
id: 3
}, {
name: 'some name4',
id: 4
}]
const filterProductsByCategoryId = (state, id) => {
return state.find(c => c.id === id);
};
const result = filterProductsByCategoryId(myData, 2);
console.log(result);

Resources