React custom country select hook - javascript

I am trying to create a reusable country select functionality by utilizing React Hooks. The idea behind the scenes is to create a function which would handle the country change within a custom select box. With this in mind, I have attempted the following code, which results in an endless loop. Is there any alternative to this code, or any ideas on how to implement it?
const { useState } = require("react");
const useCountrySelect = (defaultCountires, defaultCountry) => {
const [countries, setCountries] = useState([]);
const filteredCountries = defaultCountires.filter(
(country) => country !== defaultCountry
);
setCountries(filteredCountries);
return [countries, setCountries];
};
export default useCountrySelect;
Hook usage in a component:
const [country] = useCountrySelect(["lv", "ee", "lt"], "lt");
console.log(country);

Now, Why is your code resulting in an endless loop?
it is because when u call useCountrySelect hook it will call setCountries(filteredCountries). now state is changed. react tries re-render the component. but in the rerendering process, setCountries(filteredCountries) will be called again. this will continue like a loop.
try this,
import {useState} from 'react'
const useCountrySelect = (defaultCountires, defaultCountry) => {
const [countries, setCountries] = useState(() => {
return defaultCountires.filter(
(country) => country !== defaultCountry
);
});
return [countries, setCountries];
};
export default useCountrySelect;

setCountries(filteredCountries); is unconditionally enqueueing state updates and triggering the render looping. Just set the state initially.
const { useState } = require("react");
const useCountrySelect = (defaultCountires = [], defaultCountry) => {
const [countries, setCountries] = useState(defaultCountires.filter(
(country) => country !== defaultCountry
));
return [countries, setCountries];
};
export default useCountrySelect;
Usage:
const [country] = useCountrySelect(["lv", "ee", "lt"], "lt");
console.log(country);
Output:
["lv", "ee"]

Don't understand why your state variable is an array. It seems you want the custom hook to return the selected country as well as a function to update it.
If my understanding is correct, here is a possible solution :
const { useState } = require("react");
function useCountrySelect(countries, defaultCountry) {
const [country, setCountry] = useState(defaultCountry);
// reset the country when input countries change
useEffect(() => setCountry(defaultCountry), [countries.join()]);
function setCountryWrapper(nextCountry) {
// prevent updates with invalid country
if (!countries.includes(nextCountry) {
return;
}
setCountry(country);
}
return [country, setCountryWrapper, countries];
};
export default useCountrySelect;
Usage
const [country, setCountry] = useCountrySelect(["lv", "ee", "lt"], "lt");
console.log(country);

const { useState, useEffect } = require("react");
function filter(defaultCountires, defaultCountry) {
const filteredCountries = defaultCountires.filter(
(country) => country !== defaultCountry
);
return filteredCountries;
}
const useCountrySelect = (defaultCountires, defaultCountry) => {
const [countries, setCountries] = useState(
filter(defaultCountires, defaultCountry) // this will set the initial state
);
// side-effects like this should go inside `useEffect`
useEffect(() => {
setCountries(filter(defaultCountires, defaultCountry));
}, [defaultCountires, defaultCountry]); // based on prop change, this effect will rerun again
// don't set any state directly in the root component/hook body! Else you'll be stuck in an endless loop
return [countries, setCountries];
};
export default useCountrySelect;

Related

Why is my function not dispatching in react component?

why is fetchReviews not fetching?
Originally didn't use fetchData in use effect.
Ive tried using useDispatch.
BusinessId is being passed into the star component.
no errors in console.
please let me know if theres other files you need to see.
thank you!
star component:
import React, { useState, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import {AiFillStar } from "react-icons/ai";
import { fetchReviews } from '../../actions/review_actions';
function Star(props) {
const [rating, setRating] = useState(null);
// const [reviews, setReview] = useState(props.reviews)
// const dispatch = useDispatch();
useEffect(() => {
const fetchData = async () => {
await fetchReviews(props.businessId)
};
fetchData();
console.log(props);
// getAverageRating();
});
const getAverageRating = () => {
let totalStars = 0;
props.reviews.forEach(review => {totalStars += review.rating});
let averageStars = Math.ceil(totalStars / props.reviews.length);
setRating(averageStars);
}
return (
<div className='star-rating-container'>
{Array(5).fill().map((_, i) => {
const ratingValue = i + 1;
return (
<div className='each-star' key={ratingValue}>
<AiFillStar
className='star'
color={ratingValue <= rating ? '#D32322' : '#E4E5E9'}
size={24} />
</div>
)
})}
</div>
);
};
export default Star;
star_container:
import { connect } from "react-redux";
import { withRouter } from "react-router-dom";
import Star from "./star";
import { fetchReviews } from "../../actions/review_actions";
const mSTP = state => {
return {
reviews: Object.values(state.entities.reviews)
};
}
const mDTP = dispatch => {
return {
fetchReviews: businessId => dispatch(fetchReviews(businessId))
};
};
export default connect(mSTP, mDTP)(Star);
console image
why is fetchReviews not fetching? Originally didn't use fetchData in use effect. Ive tried using useDispatch. BusinessId is being passed into the star component. no errors in console.
edit!***
made some changes and added useDispatch. now it wont stop running. its constantly fetching.
function Star(props) {
const [rating, setRating] = useState(null);
const [reviews, setReview] = useState(null)
const dispatch = useDispatch();
useEffect(() => {
const fetchData = async () => {
const data = await dispatch(fetchReviews(props.businessId))
setReview(data);
};
fetchData();
// console.log(props);
// getAverageRating();
}), [];
ended up just calling using the ajax call in the useEffect.
useEffect(() => {
const fetchReviews = (businessId) =>
$.ajax({
method: "GET",
url: `/api/businesses/${businessId}/reviews`,
});
fetchReviews(props.businessId).then((reviews) => getAverageRating(reviews));
}), [];
if anyone knows how i can clean up and use the dispatch lmk.
ty all.
useEffect(() => {
const fetchData = async () => {
const data = await dispatch(fetchReviews(props.businessId))
setReview(data);
};
fetchData();
// console.log(props);
// getAverageRating();
}), [];
dependency array is outside the useEffect. Since useEffect has no dependency option passed, function inside useEffect will run in every render and in each render you keep dispatching action which changes the store which rerenders the component since it rerenders code inside useEffect runs
// pass the dependency array in correct place
useEffect(() => {
const fetchData = async () => {
const data = await dispatch(fetchReviews(props.businessId))
setReview(data);
};
fetchData();
// console.log(props);
// getAverageRating();
},[]), ;
Passing empty array [] means, code inside useEffect will run only once before your component mounted

react useContext setState is not a function

I'm working on a react app and I need to keep an array of names in my global state like on this file:
import React from "react";
import { useState } from "react";
const initialState = {
nameList: [],
test: 'hello'
}
export const Context = React.createContext()
const Data = ({ children }) => {
const [state, setState] = useState(initialState)
return(
<Context.Provider value={[state, setState]}>
{children}
</Context.Provider>
)
}
export default Data
However, when I try to set "nameList" to a new value in this other file:
const [state, setState] = useContext(Context);
const [currName, setCurrName] = useState('');
const handleAddName = () => {
setState.nameList(prevState => [...prevState, currName])
}
I get a "setState.nameList is not a funtion" error and I can't find nor understand the reason why, any help would be much appreciated
You're updating the state incorrectly, here's how to do it in your case:
const handleAddName = () => {
setState(prevState => ({
...prevState,
nameList: [...prevState.nameList, currName]
}))
}
This will make sure that the state is updated immutably.
setState.nameList is wrong because setState is a function but not an object that will magically have the keys of your state.

Custom React hook, infinite loop only if I add the second dependency. Bug or something I can't understand?

I've made a really simple React hook. That's something seen on many guides and websites:
import { useEffect, useState } from 'react';
import axios from 'axios';
export const useFetchRemote = (remote, options, initialDataState) => {
const [data, setData] = useState(initialDataState);
useEffect(() => {
const fetchData = async () => {
const result = await axios.get(remote, options);
setData(result.data);
};
fetchData();
}, [remote]);
return data;
};
Example usage:
import { useFetchRemote } from '../utils';
export const UserList = () => {
const users = useFetchRemote('/api/users', {}, []);
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>}
</ul>
);
}
This is working. If I understand correctly:
With no dependencies like useEffect(() => { /*...*/ }), setting the state into the function would trigger a re-render, calling useEffect again, in an infinite loop.
With empty dependencies like useEffect(() => { /*...*/ }, []), my function will be called only the "very first time" component is mounted.
So, in my case, remote is a dependency. My function should be called again if remote changes. This is true also for options. If I add also options, the infinite loop starts. I can't understand... why this is happening?
export const useFetchRemote = (remote, options, initialDataState) => {
// ...
useEffect(() => {
// ...
}, [remote, options]);
// ...
};
The infinite loop is caused by the fact that your options parameter is an object literal, which creates a new reference on every render of UserList. Either create a constant reference by defining a constant outside the scope of UserList like this:
const options = {};
const initialDataState = [];
export const UserList = () => {
// or for variable options instead...
// const [options, setOptions] = useState({});
const users = useFetchRemote('/api/users', options, initialDataState);
return (
<ul>
{users.map(user => <li key={user.id}>{user.name}</li>}
</ul>
);
}
or if you intend the options parameter to be effectively constant for each usage of the userFetchRemote() hook, you can do the equivalent of initializing props into state and prevent the reference from updating on every render:
export const useFetchRemote = (remote, options, initialDataState) => {
const [optionsState] = useState(options);
const [data, setData] = useState(initialDataState);
useEffect(() => {
const fetchData = async () => {
const result = await axios.get(remote, optionsState);
setData(result.data);
};
fetchData();
}, [remote, optionsState]);
// ---------^
return data;
};
This second approach will prevent a new fetch from occuring though, if the options are dynamically changed on a particular call site of useFetchRemote().

How to set initial state for useState Hook in jest and enzyme?

Currently Im using functional component with react hooks. But I'm unable to test the useState hook completely. Consider a scenario like, in useEffect hook I'm doing an API call and setting value in the useState. For jest/enzyme I have mocked data to test but I'm unable to set initial state value for useState in jest.
const [state, setState] = useState([]);
I want to set initial state as array of object in jest. I could not find any setState function as similar like class component.
You can mock React.useState to return a different initial state in your tests:
// Cache original functionality
const realUseState = React.useState
// Stub the initial state
const stubInitialState = ['stub data']
// Mock useState before rendering your component
jest
.spyOn(React, 'useState')
.mockImplementationOnce(() => realUseState(stubInitialState))
Reference: https://dev.to/theactualgivens/testing-react-hook-state-changes-2oga
First, you cannot use destructuring in your component. For example, you cannot use:
import React, { useState } from 'react';
const [myState, setMyState] = useState();
Instead, you have to use:
import React from 'react'
const [myState, setMyState] = React.useState();
Then in your test.js file:
test('useState mock', () => {
const myInitialState = 'My Initial State'
React.useState = jest.fn().mockReturnValue([myInitialState, {}])
const wrapper = shallow(<MyComponent />)
// initial state is set and you can now test your component
}
If you use useState hook multiple times in your component:
// in MyComponent.js
import React from 'react'
const [myFirstState, setMyFirstState] = React.useState();
const [mySecondState, setMySecondState] = React.useState();
// in MyComponent.test.js
test('useState mock', () => {
const initialStateForFirstUseStateCall = 'My First Initial State'
const initialStateForSecondUseStateCall = 'My Second Initial State'
React.useState = jest.fn()
.mockReturnValueOnce([initialStateForFirstUseStateCall, {}])
.mockReturnValueOnce([initialStateForSecondUseStateCall, {}])
const wrapper = shallow(<MyComponent />)
// initial states are set and you can now test your component
}
// actually testing of many `useEffect` calls sequentially as shown
// above makes your test fragile. I would recommend to use
// `useReducer` instead.
If I recall correctly, you should try to avoid mocking out the built-in hooks like useState and useEffect. If it is difficult to trigger the state change using enzyme's invoke(), then that may be an indicator that your component would benefit from being broken up.
SOLUTION WITH DE-STRUCTURING
You don't need to use React.useState - you can still destructure in your component.
But you need to write your tests in accordance to the order in which your useState calls are made. For example, if you want to mock two useState calls, make sure they're the first two useState calls in your component.
In your component:
import React, { useState } from 'react';
const [firstOne, setFirstOne] = useState('');
const [secondOne, setSecondOne] = useState('');
In your test:
import React from 'react';
jest
.spyOn(React, 'useState')
.mockImplementationOnce(() => [firstInitialState, () => null])
.mockImplementationOnce(() => [secondInitialState, () => null])
.mockImplementation((x) => [x, () => null]); // ensures that the rest are unaffected
Below function will return state
const setHookState = (newState) =>
jest.fn().mockImplementation(() => [
newState,
() => {},
]);
Add below to use react
const reactMock = require('react');
In your code, you must use React.useState() to this work, else it won't work
const [arrayValues, setArrayValues] = React.useState();`
const [isFetching, setFetching] = React.useState();
Then in your test add following, mock state values
reactMock.useState = setHookState({
arrayValues: [],
isFetching: false,
});
Inspiration: Goto
//Component
const MyComponent = ({ someColl, someId }) => {
const [myState, setMyState] = useState(null);
useEffect(() => {loop every time group is set
if (groupId) {
const runEffect = async () => {
const data = someColl.find(s => s.id = someId);
setMyState(data);
};
runEffect();
}
}, [someId, someColl]);
return (<div>{myState.name}</div>);
};
// Test
// Mock
const mockSetState = jest.fn();
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: initial => [initial, mockSetState]
}));
const coll = [{id: 1, name:'Test'}, {id: 2, name:'Test2'}];
it('renders correctly with groupId', () => {
const wrapper = shallow(
<MyComponent comeId={1} someColl={coll} />
);
setTimeout(() => {
expect(wrapper).toMatchSnapshot();
expect(mockSetState).toHaveBeenCalledWith({ id: 1, name: 'Test' });
}, 100);
});
I have spent a lot of time but found good solution for testing multiple useState in my app.
export const setHookTestState = (newState: any) => {
const setStateMockFn = () => {};
return Object.keys(newState).reduce((acc, val) => {
acc = acc?.mockImplementationOnce(() => [newState[val], setStateMockFn]);
return acc;
}, jest.fn());
};
where newState is object with state fields in my component;
for example:
React.useState = setHookTestState({
dataFilter: { startDate: '', endDate: '', today: true },
usersStatisticData: [],
});
I used for multiple useState() Jest mocks the following setup in the component file
const [isLoading, setLoading] = React.useState(false);
const [isError, setError] = React.useState(false);
Please note the useState mock will just work with React.useState() derivation.
..and in the test.js
describe('User interactions at error state changes', () => {
const setStateMock = jest.fn();
beforeEach(() => {
const useStateMock = (useState) => [useState, setStateMock];
React.useState.mockImplementation(useStateMock)
jest.spyOn(React, 'useState')
.mockImplementationOnce(() => [false, () => null]) // this is first useState in the component
.mockImplementationOnce(() => [true, () => null]) // this is second useState in the component
});
it('Verify on list the state error is visible', async () => {
render(<TodoList />);
....
NOT CHANGING TO React.useState
This approach worked for me:
//import useState with alias just to know is a mock
import React, { useState as useStateMock } from 'react'
//preseve react as it actually is but useState
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn(),
}))
describe('SearchBar', () => {
const realUseState: any = useStateMock //create a ref copy (just for TS so it prevents errors)
const setState = jest.fn() //this is optional, you can place jest.fn directly
beforeEach(() => {
realUseState.mockImplementation((init) => [init, setState]) //important, let u change the value of useState hook
})
it('it should execute setGuestPickerFocused with true given that dates are entered', async () => {
jest
.spyOn(React, 'useState')
.mockImplementationOnce(() => ['', () => null]) //place the values in the order of your useStates
.mockImplementationOnce(() => ['20220821', () => null]) //...
.mockImplementationOnce(() => ['20220827', () => null]) //...
jest.spyOn(uiState, 'setGuestPickerFocused').mockReturnValue('')
getRenderedComponent()
expect(uiState.setGuestPickerFocused).toHaveBeenCalledWith(true)
})
})
My component
const MyComp: React.FC<MyCompProps> = ({
a,
b,
c,
}) => {
const [searchQuery, setSearchQuery] = useState('') // my first value
const [startDate, setStartDate] = useState('') // my second value
const [endDate, setEndDate] = useState('') // my third value
useEffect(() => {
console.log(searchQuery, startDate, endDate) // just to verifiy
}, [])
Hope this helps!

Can not set a state in a react component

I have a react component. I want to set the state within this component that will be passed down to child components. I am getting a reference error to this and I am not sure why.
export const WidgetToolbar: React.FC<{}> = () => {
this.state = {
targetBox:null,
}
const isOpen = useBehavior(mainStore.isWidgetToolbarOpen);
const themeClass = useBehavior(mainStore.themeClass);
const userDashboards = useBehavior(dashboardStore.userDashboards);
const [filter, setFilter] = useState("");
const [sortOrder, setSortOrder] = useState<SortOrder>("asc");
const userWidgets = useMemo(() => {
let _userWidgets = values(userDashboards.widgets).filter((w) => w.widget.isVisible);
if (sortOrder === "asc") {
_userWidgets.sort((a, b) => a.widget.title.localeCompare(b.widget.title));
} else {
_userWidgets.sort((a, b) => b.widget.title.localeCompare(a.widget.title));
}
if (!isBlank(filter)) {
_userWidgets = _userWidgets.filter((row) => {
return row.widget.title.toLowerCase().includes(filter.toLocaleLowerCase());
});
}
return _userWidgets;
}, [userDashboards, sortOrder, filter]);
...
This is the error I am getting:
TypeError: Cannot set property 'state' of undefined
at WidgetToolbar (WidgetToolbar.tsx?ba4c:25)
at ProxyFacade (react-hot-loader.development.js?439b:757)
There's no this or this.state in a functional component. Use the useState hook, similar to what you're doing a few lines below.
export const WidgetToolbar: React.FC<{}> = () => {
const [targetBox, setTargetBox] = useState<null | whateverTheTypeIs>(null);
//...
}
Functional React Components can't have state. You'd have to use a class-based component in order to have state.
https://guide.freecodecamp.org/react-native/functional-vs-class-components/
You used the hook to "use state" in this function: const [filter, setFilter] = useState("");
You could do the same for targetBox, instead of trying to set a property on a non-existent 'this'

Categories