React: How do i fix "Maximum update depth exceeded error" - javascript

I have 2 modals and they are similar. One of works but the other one sometimes works sometimes doesn't. Both codes are same just the models is different but i coulnd't find any result.
This is error;
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
174 | }
175 | setVals(data.form);
176 |
177 | setInputList(
| ^ 178 | JSON.parse(data.form.cabinTeam, data.form.cabinTeamLicence)
179 | );
This is code;
useEffect(() => {
setDisabled(false);
setVals(initVals);
setInputList([{ cabinTeam: "", cabinTeamLicence: "" }]);
if (id) {
axios.get(`/twoA/${id}`).then(({ data }) => {
if (data.form !== null && data.form !== undefined) {
if (data.form.opearationDate !== null) {
data.form.opearationDate = moment(data.form.opearationDate).format(
"DD/MM/YYYY"
);
}
setVals(data.form);
setInputList(
JSON.parse(data.form.cabinTeam, data.form.cabinTeamLicence)
);
setDisabled(true);
}
});
}
}, [id]);
const [inputList, setInputList] = useState([
{ cabinTeam: "", cabinTeamLicence: "" },
]);
const handleInputChange = (e, index) => {
const { name, value } = e.target;
const list = [...inputList];
list[index][name] = value;
setInputList(list);
};
const handleRemoveClick = (index) => {
const list = [...inputList];
list.splice(index, 1);
setInputList(list);
};
const handleAddClick = () => {
setInputList([...inputList, { cabinTeam: "", cabinTeamLicence: "" }]);
};

Related

Routing is not triggerd with useEffect in Nextjs

I have a dynamic page in Nextjs -> steps -> [slug].tsx . Essentially, it is a steps page and routing is triggered whenever a step changes in the state. For example, if I am in the first step, its value is null but once I click next its value becomes "next". When I go back I basically null the step and useEffect should react to these changes and route. However, it doesn't seem to work. I am not sure why.
Here is my code
type Steps = {
title: string;
slug: string;
value: string | null;
};
const StepsPage: NextPage = () => {
const router = useRouter();
const { query } = router;
const [steps, setSteps] = useState<Steps[]>([
{
title: "Step 1",
slug: "step-1",
value: null,
},
{
title: "Step 2",
slug: "step-2",
value: null,
},
{
title: "Step 3",
slug: "step-3",
value: null,
},
]);
let step: Steps | undefined = undefined;
useEffect(() => {
if (!steps) {
return;
}
const nextStep = steps.find((step) => step.value === null);
if (nextStep && step && step.slug !== nextStep.slug) {
console.log("go to next step");
router.push(`/steps/${nextStep.slug}`);
return;
}
if (!nextStep) {
router.push("/result");
return;
}
}, [router, steps, step]);
if (!steps) {
<div>no steps found</div>;
}
const slug = Array.isArray(query.slug) ? query.slug[0] : query.slug;
if (!slug) {
return <div>no slug found</div>;
}
const currentStep = steps.find((step) => step.slug === slug);
if (!currentStep) {
return <div>no current step found</div>;
}
const handleStep = (value: string | null) => {
setSteps((prevSteps) => {
const updatedSteps = prevSteps.map((step) => {
if (step.slug === slug) {
return { ...step, value };
}
return step;
});
return updatedSteps;
});
};
return (
<div>
<h1>Step Page</h1>
<p>Slug: {slug}</p>
<button onClick={() => handleStep("next")}>Next</button>
<button onClick={() => handleStep(null)}>Back</button>
</div>
);
};
export default StepsPage;
To me, it looks like the logic might just be a little jumbled. Here's what I'd recommend.
First, it looks like the steps themselves are static. I would make your Step type simply
type StepSlug = "step1" | "step2" | "step3"; // Extra type safety
interface Step {
title: string;
slug: StepSlug;
};
Then, I would declare the following array outside of the component, since it is static.
const STEPS: Step[] = [
step1: {
title: "Step One",
slug: "step1",
},
// same for step2 and step3
];
Then, within your component, you have the following state:
const StepsPage = () => {
...
const [stepIx, setStepIx] = useState(0);
const [values, setValues] = useState<(string | null)[]>(STEPS.map(() => null)); // creates initial values of array, same length as STEPS, with all null values
...
// Not necessary but for readability
const currentStep = STEPS[stepIx];
const canGoForward = stepIx < STEPS.length - 1;
const canGoBackward = stepIx > 0;
const goForward = () => {
if (!canGoForward) return;
setStepIx((val) => val + 1);
}
const goBackward = () => {
if (!canGoBackward) return;
setStepIx((val) => val - 1);
}
// Helper functions
const updateCurrentValue = (newVal: string) => {
setValues((oldValues) => {
oldValues[stepIx] = newVal;
return oldValues;
}
};
...
// Here's how you listen to changes to the step
useEffect(() => {
... // logic for setting the slug
}, [stepIx]);
...
}
Obviously there's still a bit to fill in, but this is both safer (w.r.t types) and lets stepIx serve as the source of truth combined with the STEPS array so you don't have to keep searching for the slug.

React Native app has low performance while fetching data from firebase

Hello I have a react native app that gets slow/freeze while fetching data from realtime db.
This is only on android devices, on IOs the app works fine.
for example, by using the performance monitor in my physical device I get:
UI: 59 fps
79 DROPEDD
3 Stuters fo far
JS: -2.1 fps
I get the data in the App.js as the following code, which it is inside an useEffect with an empty dependency array:
async function fetchData() {
try {
const [
postFetched,
categoriesFetched,
assetsFetched,
] = await Promise.all([getAllPosts(), getCategories(), getAssets()]);
const email = await AsyncStorage.getItem('#email');
//Alert.alert(`whats uid ? ${email}`);
if (email) {
const userFetched = await searchUserByEmail(email);
setUser(Object.values(userFetched.val())[0]);
}
// eslint-disable-next-line no-undef
const t2 = performance.now();
//Alert.alert(`time ${(t2 - t1) / 1000} seg`);
return {postFetched, categoriesFetched, assetsFetched};
} catch (error) {
throw new Error(error.toString());
}
}
fetchData()
.then((data) => {
const {postFetched, categoriesFetched, assetsFetched} = data;
if (postFetched.exists()) {
setPosts(
orderBy(
toArray(postFetched.val()),
['isTop', 'created'],
['desc', 'desc'],
),
);
//setLoading(false);
}
if (categoriesFetched.exists()) {
setCategories(toArray(categoriesFetched.val()));
}
if (assetsFetched.exists()) {
setAssetsFirebase(assetsFetched.val());
// SplashScreenNative.hide();
}
// SplashScreenNative.hide();
/*if (Platform.OS === 'android') {
setIsLoading(false);
}*/
})
.catch((error) => {
Toast.show({
type: 'error',
text1: 'error ' + error.toString(),
});
console.log('error', error);
});
listenPostsChange(); // -> this also inside the same use effect
the listenPostChange function:
const listenPostsChange = () => {
listenPosts((data) => {
data.exists() &&
setPosts(
orderBy(toArray(data.val()), ['isTop', 'created'], ['desc', 'desc']),
);
});
};
the firebase methods are defined in a separate js file for each reference:
post.js
import database from '#react-native-firebase/database';
const PostsRef = database().ref('/posts');
export const listenPosts = (callbackFunction) => {
return PostsRef.on('value', callbackFunction);
};
export const listenPosts = (callbackFunction) => {
return PostsRef.on('value', callbackFunction);
};
category.js
import database from '#react-native-firebase/database';
const CategoriesRef = database().ref('/categories');
export const addCategory = (label, category) => {
return CategoriesRef.child(label).update({...category});
};
export const getCategories = () => {
return CategoriesRef.once('value', (snapshot) =>
snapshot.exists() ? snapshot.val() : [],
);
};
assets.js
import database from '#react-native-firebase/database';
const AssetsRef = database().ref('/assets');
export const getAssets = () => {
return AssetsRef.once('value', (snapshot) =>
snapshot.exists() ? snapshot.val() : [],
);
};
I have the following dependencies installed in my app:
"#react-native-firebase/app": "^14.2.2",
"#react-native-firebase/auth": "^14.2.2",
"#react-native-firebase/database": "^14.11.0",
"#react-native-firebase/storage": "^14.11.0",
"firebase": "8.10.1",
I think the main problem is related to firebase since when I deleted the fetchData and the listenPostsChange functions, the app worked smoothly but without data to show.
I hope someone can help me
thanks.
edit: I just have like 15 post and 6 categories, and the assets are just 3 strings values.

Dealing with post requests and tokens while testing a React application with Jest

This is my first time working with jest and react. I created an application that requires a user to be authenticated to access certain resources.
I created the following function in the Login component for authentication that makes an axios call to the server-
const loginUtil = async() => {
const res = await axios
.post("http://localhost:8080/user/authenticate", {
username,
password,
});
console.log(res.data);
if (res.data.jwt) {
localStorage.setItem("user", JSON.stringify(res.data));
} else {
throw new Error();
}
return JSON.stringify(res.data);
}
return loginUtil();
};
I wrote the following two Jest test case for the component -
import * as React from 'react'
import { rest } from 'msw'
import { setupServer } from 'msw/node'
import { render, fireEvent, screen } from '#testing-library/react'
import Login from '../Login'
import {jest} from '#jest/globals'
const fakeUserResponse = {jwt: 'fake_user_token',user : {
username : 'jane',
pasword : 'pass',
role : 'user',
id : 'ftrb3344grr'
}}
const server = setupServer(
rest.post('http://localhost:8080/user/authenticate', (req, res, ctx) => {
return res(ctx.json(fakeUserResponse))
}),
);
beforeAll(() => server.listen())
beforeEach(() => {
jest.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
server.resetHandlers()
window.localStorage.removeItem('user')
})
afterAll(() => server.close())
test('token is saved correctly', async() => {
render(<Login />)
fireEvent.change(screen.getByPlaceholderText(/username/i), {
target: {value: 'jane'},
})
fireEvent.change(screen.getByPlaceholderText(/password/i), {
target: {value: 'pass'}
})
fireEvent.click(screen.getByTestId(/login/i))
expect(localStorage.getItem('user')).toEqual(fakeUserResponse)
})
The first test case passes but the second doesn't. I'm also getting the following error -
Error message in the console
FAIL src/components/__tests__/Login.test.js (6.919 s)
● token is saved correctly
expect(received).toEqual(expected) // deep equality
Expected: {"jwt": "fake_user_token", "user": {"id": "ftrb3344grr", "pasword": "pass", "role": "user", "username": "jane"}}
Received: null
61 | fireEvent.click(screen.getByTestId(/login/i))
62 |
> 63 | expect(localStorage.getItem('user')).toEqual(fakeUserResponse)
| ^
64 | })
65 |
at Object.<anonymous> (src/components/__tests__/Login.test.js:63:42)
● Cannot log after tests are done. Did you forget to wait for something async in your test?
Attempted to log "{
jwt: 'fake_user_token',
user: {
username: 'jane',
pasword: 'pass',
role: 'user',
id: 'ftrb3344grr'
}
}".
16 | } else {
17 | throw new Error();
> 18 | }
| ^
19 | return JSON.stringify(res.data);
20 | }
21 |
at BufferedConsole.log (node_modules/#jest/console/build/BufferedConsole.js:197:10)
at loginUtil (src/service/authService.js:18:13)
What this tells me is the response data is correctly sent by the mock server but for some reason the data can't be logged into the console. The test stops giving expected results beyond that.
Any kind of help regarding why this is happening would be appreciated.
Thank you in advance.
You can just mock local storage, for your use. Here is an example for it.
//Storage Mock
const storageMock = () => {
let storage = {};
return {
setItem:(key, value) => {
storage[key] = value || '';
},
getItem: (key) => {
return storage[key] || null;
},
removeItem: (key) => {
delete storage[key];
},
clear: function() {
store = {};
},
getLength: () => {
return Object.keys(storage).length;
},
key: function(i) {
const keys = Object.keys(storage);
return keys[i] || null;
}
};
}
And the you do something like:
// mock the localStorage
window.localStorage = storageMock();
// mock the sessionStorage
window.sessionStorage = storageMock();

Scroll bar keep returning to top in infinite scroll -React js

I have used following code in my component to load new data when scrolling.But when new page of data is loaded the scroll bar keeps returning to the top.I'm using this for Magento PWA with react.
const Category = props => {
const {id} = props;
const classes = mergeClasses(defaultClasses, props.classes);
const [paginationValues, paginationApi] = usePagination();
const {currentPage, totalPages} = paginationValues;
const {setCurrentPage, setTotalPages} = paginationApi;
const [selectedPageSize, setPageSize] = useState(40);
const [pageNumber, setPageNumber] = useState(1);
function handlePageSize() {
setPageNumber(pageNumber+1)
}
const sortProps = useSort();
const [currentSort] = sortProps;
const previousSort = useRef(currentSort);
const pageControl = {
currentPage,
setPage: setCurrentPage,
totalPages
};
const [runQuery, queryResponse] = useLazyQuery(GET_CATEGORY,{fetchPolicy:'cache-first'});
const {loading, error, data} = queryResponse;
const {search} = useLocation();
const bqueryResponsee = useQuery(
GET_ATTRIBUTES
);
let battributedata = '';
// Keep track of the search terms so we can tell when they change.
const previousSearch = useRef(search);
// Get "allowed" filters by intersection of schema and aggregations
const {data: introspectionData} = useQuery(FILTER_INTROSPECTION);
// Create a type map we can reference later to ensure we pass valid args
// to the graphql query.
// For example: { category_id: 'FilterEqualTypeInput', price: 'FilterRangeTypeInput' }
const filterTypeMap = useMemo(() => {
const typeMap = new Map();
if (introspectionData) {
introspectionData.__type.inputFields.forEach(({name, type}) => {
typeMap.set(name, type.name);
});
}
return typeMap;
}, [introspectionData]);
// Run the category query immediately and whenever its variable values change.
useEffect(() => {
window.addEventListener('scroll', infiniteScroll);
// Wait until we have the type map to fetch product data.
if (!filterTypeMap.size) {
return;
}
const filters = getFiltersFromSearch(search);
const newFilters = {};
newFilters['category_id'] = {eq: String(id)};
filters.forEach((values, key) => {
newFilters[key] = getFilterInput(values, filterTypeMap.get(key));
});
runQuery({
variables: {
currentPage: Number(pageNumber),
id: Number(id),
filters: newFilters,
pageSize: Number(selectedPageSize),
sort: {[currentSort.sortAttribute]: currentSort.sortDirection}
}
});
window.scrollTo({
left: 0,
top: 0,
behavior: 'smooth'
});
}, [
currentPage,
currentSort,
filterTypeMap,
id,
selectedPageSize,
runQuery,
search,
pageNumber
]);
const totalPagesFromData = data
? Math.ceil(data.category.product_count/selectedPageSize)
: null;
useEffect(() => {
setTotalPages(totalPagesFromData);
return () => {
setTotalPages(null);
};
}, [setTotalPages, totalPagesFromData]);
// If we get an error after loading we should try to reset to page 1.
// If we continue to have errors after that, render an error message.
useEffect(() => {
if (error && !loading && currentPage !== 1) {
setCurrentPage(1);
}
}, [currentPage, error, loading, setCurrentPage]);
// Reset the current page back to one (1) when the search string, filters
// or sort criteria change.
useEffect(() => {
// We don't want to compare page value.
const prevSearch = new URLSearchParams(previousSearch.current);
const nextSearch = new URLSearchParams(search);
prevSearch.delete('page');
nextSearch.delete('page');
if (
prevSearch.toString() !== nextSearch.toString() ||
previousSort.current.sortAttribute.toString() !==
currentSort.sortAttribute.toString() ||
previousSort.current.sortDirection.toString() !==
currentSort.sortDirection.toString()
) {
// The search term changed.
setCurrentPage(1);
// And update the ref.
previousSearch.current = search;
previousSort.current = currentSort;
}
}, [currentSort, previousSearch, search, setCurrentPage]);
if (error && currentPage === 1 && !loading) {
if (process.env.NODE_ENV !== 'production') {
console.error(error);
}
return <div>Data Fetch Error</div>;
}
// Show the loading indicator until data has been fetched.
if (totalPagesFromData === null) {
return fullPageLoadingIndicator;
}
if (typeof bqueryResponsee.data !== "undefined" && !bqueryResponsee.loading) {
battributedata = bqueryResponsee.data.customAttributeMetadata.items[0].attribute_options;
} else {
battributedata = [];
}
const count = totalPagesFromData ? totalPagesFromData : null;
const galleries = [];
if (count && count >= 1) {
for (let i =1;i<=pageNumber;i++) {
galleries.push(
<section className={classes.gallery}>
<Gallery searchItems={null} ID={1}
newSort={{[currentSort.sortAttribute]: currentSort.sortDirection}}
newFilters={newFiltersLazy}
currentPage={i} categoryId={id} pageSize={40} introspectionData={introspectionData}
battributedata={battributedata}/>
</section>
)
}
}
function infiniteScroll(){
console.log(Math.ceil((window.innerHeight + document.documentElement.scrollTop)/100)*100,Math.ceil((document.documentElement.offsetHeight*8/10)/100)*100)
// End of the document reached?
if ( Math.ceil((window.innerHeight + document.documentElement.scrollTop)/100)*100
>= Math.ceil((document.documentElement.offsetHeight*8/10)/100)*100){
handlePageSize()
}
}
return (
<Fragment>
<Meta name="description" content={metaDescription}/>
<CategoryContent
totalPagesFromData={totalPagesFromData}
categoryId={id}
classes={classes}
data={loading ? null : data}
pageControl={pageControl}
sortProps={sortProps}
onSelectSize={handlePageSize}
battributedata={battributedata}
selectedPageSize={selectedPageSize}
galleries={galleries}
newSort={{[currentSort.sortAttribute]: currentSort.sortDirection}}
newFilters={newFiltersLazy}
introspectionData={introspectionData}
pageNumber={pageNumber}
/>
</Fragment>
);
};
What I did here is created a new array galleries and the Gallery component in each page increment while scrolling and inside Gallery the items will be created.Problem is the scroll bar keep returning to the top in each increment.Please help

React Native Flatlist Not Rerendering Redux

My FlatList does not update when the props I pass from redux change. Every time I send a message I increase everyones unread message count in both firebase and in my redux store. I made sure to include key extractor and extra data, but neither helps. The only thing that changes the unread message count is a reload of the device. How do I make sure the flatList updates with MapStateToProps. I made sure to create a new object by using Object.Assign:
action:
export const sendMessage = (
message,
currentChannel,
channelType,
messageType
) => {
return dispatch => {
dispatch(chatMessageLoading());
const currentUserID = firebaseService.auth().currentUser.uid;
let createdAt = firebase.database.ServerValue.TIMESTAMP;
let chatMessage = {
text: message,
createdAt: createdAt,
userId: currentUserID,
messageType: messageType
};
FIREBASE_REF_MESSAGES.child(channelType)
.child(currentChannel)
.push(chatMessage, error => {
if (error) {
dispatch(chatMessageError(error.message));
} else {
dispatch(chatMessageSuccess());
}
});
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child(channelType)
.child(currentChannel).child('users')
UNREAD_MESSAGES.once("value")
.then(snapshot => {
snapshot.forEach(user => {
let userKey = user.key;
// update unread messages count
if (userKey !== currentUserID) {
UNREAD_MESSAGES.child(userKey).transaction(function (unreadMessages) {
if (unreadMessages === null) {
dispatch(unreadMessageCount(currentChannel, 1))
return 1;
} else {
alert(unreadMessages)
dispatch(unreadMessageCount(currentChannel, unreadMessages + 1))
return unreadMessages + 1;
}
});
} else {
UNREAD_MESSAGES.child(userKey).transaction(function () {
dispatch(unreadMessageCount(currentChannel, 0))
return 0;
});
}
}
)
})
};
};
export const getUserPublicChannels = () => {
return (dispatch, state) => {
dispatch(loadPublicChannels());
let currentUserID = firebaseService.auth().currentUser.uid;
// get all mountains within distance specified
let mountainsInRange = state().session.mountainsInRange;
// get the user selected mountain
let selectedMountain = state().session.selectedMountain;
// see if the selected mountain is in range to add on additional channels
let currentMountain;
mountainsInRange
? (currentMountain =
mountainsInRange.filter(mountain => mountain.id === selectedMountain)
.length === 1
? true
: false)
: (currentMountain = false);
// mountain public channels (don't need to be within distance)
let currentMountainPublicChannelsRef = FIREBASE_REF_CHANNEL_INFO.child(
"Public"
)
.child(`${selectedMountain}`)
.child("Public");
// mountain private channels- only can see if within range
let currentMountainPrivateChannelsRef = FIREBASE_REF_CHANNEL_INFO.child(
"Public"
)
.child(`${selectedMountain}`)
.child("Private");
// get public channels
return currentMountainPublicChannelsRef
.orderByChild("key")
.once("value")
.then(snapshot => {
let publicChannelsToDownload = [];
snapshot.forEach(channelSnapshot => {
let channelId = channelSnapshot.key;
let channelInfo = channelSnapshot.val();
// add the channel ID to the download list
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child("Public")
.child(channelId).child('users').child(currentUserID)
UNREAD_MESSAGES.on("value",snapshot => {
if (snapshot.val() === null) {
// get number of messages in thread if haven't opened
dispatch(unreadMessageCount(channelId, 0));
} else {
dispatch(unreadMessageCount(channelId, snapshot.val()));
}
}
)
publicChannelsToDownload.push({ id: channelId, info: channelInfo });
});
// flag whether you can check in or not
if (currentMountain) {
dispatch(checkInAvailable());
} else {
dispatch(checkInNotAvailable());
}
// if mountain exists then get private channels/ if in range
if (currentMountain) {
currentMountainPrivateChannelsRef
.orderByChild("key")
.on("value", snapshot => {
snapshot.forEach(channelSnapshot => {
let channelId = channelSnapshot.key;
let channelInfo = channelSnapshot.val();
const UNREAD_MESSAGES = FIREBASE_REF_UNREAD.child("Public")
.child(channelId).child('users').child(currentUserID)
UNREAD_MESSAGES.on("value",
snapshot => {
if (snapshot.val() === null) {
// get number of messages in thread if haven't opened
dispatch(unreadMessageCount(channelId, 0));
} else {
dispatch(unreadMessageCount(channelId, snapshot.val()));
}
}
)
publicChannelsToDownload.push({ id: channelId, info: channelInfo });
});
});
}
return publicChannelsToDownload;
})
.then(data => {
setTimeout(function () {
dispatch(loadPublicChannelsSuccess(data));
}, 150);
});
};
};
Reducer:
case types.UNREAD_MESSAGE_SUCCESS:
const um = Object.assign(state.unreadMessages, {[action.info]: action.unreadMessages});
return {
...state,
unreadMessages: um
};
Container- inside I hook up map state to props with the unread messages and pass to my component as props:
const mapStateToProps = state => {
return {
publicChannels: state.chat.publicChannels,
unreadMessages: state.chat.unreadMessages,
};
}
Component:
render() {
// rendering all public channels
const renderPublicChannels = ({ item, unreadMessages }) => {
return (
<ListItem
title={item.info.Name}
titleStyle={styles.title}
rightTitle={(this.props.unreadMessages || {} )[item.id] > 0 && `${(this.props.unreadMessages || {} )[item.id]}`}
rightTitleStyle={styles.rightTitle}
rightSubtitleStyle={styles.rightSubtitle}
rightSubtitle={(this.props.unreadMessages || {} )[item.id] > 0 && "unread"}
chevron={true}
bottomDivider={true}
id={item.Name}
containerStyle={styles.listItemStyle}
/>
);
};
return (
<View style={styles.channelList}>
<FlatList
data={this.props.publicChannels}
renderItem={renderPublicChannels}
keyExtractor={(item, index) => index.toString()}
extraData={[this.props.publicChannels, this.props.unreadMessages]}
removeClippedSubviews={false}
/>
</View>
);
}
}
Object.assign will merge everything into the first object provided as an argument, and return the same object. In redux, you need to create a new object reference, otherwise change is not guaranteed to be be picked up. Use this
const um = Object.assign({}, state.unreadMessages, {[action.info]: action.unreadMessages});
// or
const um = {...state.unreadMessages, [action.info]: action.unreadMessages }
Object.assign() does not return a new object. Due to which in the reducer unreadMessages is pointing to the same object and the component is not getting rerendered.
Use this in your reducer
const um = Object.assign({}, state.unreadMessages, {[action.info]: action.unreadMessages});

Categories

Resources