I have this component that returns a bunch of li's on a axios get request, users input text and the search is updated..I want React to re-render the component when the searchInput is null, basically back to its original state.
class App extends Component {
constructor (props) {
super(props)
this.state = {
searchResults: [], // API Call returns an array of results
searchInput: '', // Search Term for API Call
searchImage: [] //base_url, a file_size and a file_path.
}
}
performSearch = () => { // Requesting data from API
axios.get(`${URL}api_key=${API_KEY}&language=en-US&query=${this.state.searchInput}${PARAMS}`)
.then((res) => {
console.log(res.data.results);
this.setState({ searchResults: res.data.results});
});
}
This function below is what triggers the rendering
handleInputChange = () => {
this.setState({
searchInput: this.search.value // User input
}, () => {
if (this.state.searchInput && this.state.searchInput.length >1 ) {
if (this.state.searchInput.length % 2 === 0) { // Request data on user input
this.performSearch();
} else if (this.state.searchInput && this.state.searchInput.length === 0 ) {
return ({ searchResults: null})
}
}
});
}
import React from 'react'
const Suggestions = (props) => {
const options = props.searchResults.map(r => (
<li
key={r.id} >
<img src={`https://image.tmdb.org/t/p/w185/${r.poster_path}`} alt={r.title} />
<a href='#t' className='rating'><i className='fas fa-star fa-fw' />{r.vote_average}</a>
</li>
))
return <ul className='search-results'>{options}</ul>
}
export default Suggestions
Issue at the moment is that is that if I search something, eg 'game of thrones' it renders the li's, however if I clear it back to an empty string, I still have left over li's...I dont't wanna see anything if the searchInput is null
Edit: performSearch is fires again while clearing searchInput and returns the last two characters which leaves me with left over li's
You haven't handled the conditions correct in the handleInputChange method. If the outer condition fails, the inner won't ever execute
handleInputChange = () => {
this.setState({
searchInput: this.search.value // User input
}, () => {
if (this.state.searchInput && this.state.searchInput.length >1 ) {
if (this.state.searchInput.length % 2 === 0) { // Request data on user input
this.performSearch();
}
} else {
this.now = Date.now();
this.setState({ searchResults: []})
}
});
}
Also the issue here could possible be the race condition with the API calls. It might so happen that when you clear the input although you setState to null or empty, the API then responds which sets the state again. The best way to handle is to accept only response that corresponds to the last request made
performSearch = () => { // Requesting data from API
let now = (this.now = Date.now());
axios.get(`${URL}api_key=${API_KEY}&language=en-US&query=${this.state.searchInput}${PARAMS}`)
.then((res) => {
console.log(res.data.results);
// Accepting response if this request was the last request made
if (now === this.now) {
this.setState({ searchResults: res.data.results});
}
});
}
Related
The useEffect doesn't fire on first render, but when I save the file (ctrl+s), the state updates and the results can be seen.
What I want to do is, when I'm in GameScreen, I tap on an ICON which takes me to WalletScreen, from there I can select some items/gifts (attachedGifts - in context) and after finalising I go back to previous screen i.e. GameScreen with gifts attached (attachedGifts!==null), now again when I tap ICON and go to WalletScreen it should show me the gifts that were attached so that I could un-attach them or update selection (this is being done in the useEffect below in WalletScreen), but the issue is, although my attachedGifts state is updating, the useEffect in WalletScreen does not fire immediately when navigated, when I hit ctrl+s to save the file, then I can see my selected/attached gifts in WalletScreen.
code:
const Main = () => {
return (
<GiftsProvider>
<Stack.Screen name='WalletScreen' component={WalletScreen} />
<Stack.Screen name='GameScreen' component={GameScreen} />
</GiftsProvider>
)
};
const GameScreen = () => {
const { attachedGifts } = useGifts(); //coming from context - GiftsProvider
console.log('attached gifts: ', attachedGifts);
return ...
};
const WalletScreen = () => {
const { attachedGifts } = useGifts();
useEffect(() => { // does not fire on initial render, after saving the file, then it works.
if (attachedGifts !== null) {
let selectedIndex = -1
let filteredArray = data.map(val => {
if (val.id === attachedGifts.id) {
selectedIndex = walletData.indexOf(val);
setSelectedGiftIndex(selectedIndex);
return {
...val,
isSelect: val?.isSelect ? !val?.isSelect : true,
};
} else {
return { ...val, isSelect: false };
}
});
setData(filteredArray);
}
}, [attachedGifts]);
const attachGiftsToContext = (obj) => {
dispatch(SET_GIFTS(obj));
showToast('Gifts attached successfully!');
navigation?.goBack(); // goes back to GameScreen
}
return (
// somewhere in between
<TouchableOpacity onPress={attachGiftsToContext}>ATTACH</TouchableOpacity>
)
};
context:
import React, { createContext, useContext, useMemo, useReducer } from 'react';
const GiftsReducer = (state: Object | null, action) => {
switch (action.type) {
case 'SET_GIFTS':
return action.payload;
default:
return state;
}
};
const GiftContext = createContext({});
export const GiftsProvider = ({ children }) => {
const initialGiftState: Object | null = null;
const [attachedGifts, dispatch] = useReducer(
GiftsReducer,
initialGiftState,
);
const memoedValue = useMemo(
() => ({
attachedGifts,
dispatch,
}),
[attachedGifts],
);
return (
<GiftContext.Provider value={memoedValue}>
{children}
</GiftContext.Provider>
);
};
export default function () {
return useContext(GiftContext);
}
Output of console.log in GameScreen:
attached gifts: Object {
"reciptId": "baNlCz6KFVABxYNHAHasd213Fu1",
"walletId": "KQCqSqC3cowZ987663QJboZ",
}
What could possibly be the reason behind this and how do I solve this?
EDIT
Added related code here: https://snack.expo.dev/uKfDPpNDr
From the docs
When you call useEffect in your component, this is effectively queuing
or scheduling an effect to maybe run, after the render is done.
After rendering finishes, useEffect will check the list of dependency
values against the values from the last render, and will call your
effect function if any one of them has changed.
You might want to take a different approach to this.
There is not much info, but I can try to suggest to put it into render, so it might look like this
const filterAttachedGifts = useMemo(() => ...your function from useEffect... , [attachedGitfs])
Some where in render you use "data" variable to render attached gifts, instead, put filterAttachedGifts function there.
Or run this function in component body and then render the result.
const filteredAttachedGifts = filterAttachedGifts()
It would run on first render and also would change on each attachedGifts change.
If this approach doesn't seems like something that you expected, please, provide more code and details
UPDATED
I assume that the problem is that your wallet receive attachedGifts on first render, and after it, useEffect check if that value was changed, and it doesn't, so it wouldn't run a function.
You can try to move your function from useEffect into external function and use that function in 2 places, in useEffect and in wallet state as a default value
feel free to pick up a better name instead of "getUpdatedArray"
const getUpdatedArray = () => {
const updatedArray = [...walletData];
if (attachedGifts !== null) {
let selectedIndex = -1
updatedArray = updatedArray.map((val: IWalletListDT) => {
if (val?.walletId === attachedGifts?.walletIds) {
selectedIndex = walletData.indexOf(val);
setSelectedGiftIndex(selectedIndex);
setPurchaseDetailDialog(val);
return {
...val,
isSelect: val?.isSelect ? !val?.isSelect : true,
};
} else {
return { ...val, isSelect: false };
}
});
}
return updatedArray;
}
Then use it here
const [walletData, setWalletData] = useState(getUpdatedArray());
and in your useEffect
useEffect(() => {
setWalletData(getUpdatedArray());
}, [attachedGifts]);
That update should cover the data on first render. That might be not the best solution, but it might help you. Better solution require more code\time etc.
My goal is to build a simple product review system using React, Next.JS and the browser's sessionStorage.
The user should be able to click on a button to "Add a review". This action will trigger the display of a text area and a submit button. Once the user click the submit button, the review content should be persisted in the sessionStorage and immediately showed up in a list of reviews.
My problem is that although I can update the sessionStorage after submitting the review, the app is not displaying the list of existing reviews right away.
If I leave the page and get back, the reviews will be shown up, meaning my custom hook seems to be working fine.
Here's the ReviewForm.tsx code:
export const ReviewForm: React.FC<Props> = ({ productId }): JSX.Element => {
const [showForm, setShowForm] = useState<boolean>(false);
const [storedValues, setStoredValues] = useSessionStorage<SessionStorage[]>(
"products-reviews",
[]
);
const registerReview = (event: any) => {
event.preventDefault();
const reviewText = event.target.review.value;
const productIndex = storedValues?.findIndex(
(review) => review.productId === productId
);
if (productIndex === -1 || productIndex === undefined) {
setStoredValues([...storedValues!, { productId, reviews: [reviewText] }]);
} else {
const reviews = [...storedValues![productIndex].reviews, reviewText];
const updatedReviews = [...storedValues!];
updatedReviews[productIndex].reviews = reviews;
setStoredValues(updatedReviews);
}
setShowForm(false);
};
return (
<div className={styles.reviewsContainer}>
<button
className={styles.addReviewButton}
onClick={() => setShowForm(true)}
>
<span>Add a review</span>
</button>
{showForm && (
<form
className={styles.reviewForm}
onSubmit={(event) => registerReview(event)}
>
<textarea className={styles.reviewInput} name="review" required />
<button className={styles.reviewSubmitButton} type="submit">
Submit
</button>
</form>
)}
<ReviewList productId={productId} />
</div>
);
};
And here's the ReviewList.tsx component, rendered inside ReviewForm.tsx:
export const ReviewList: React.FC<Props> = ({ productId }): JSX.Element => {
const [reviews, _] = useSessionStorage<SessionStorage[]>(
"products-reviews",
[]
);
const productReviews = reviews?.find(
(review) => review.productId === productId
)?.reviews;
return (
<ul>
{productReviews?.map((review) => (
<li key={Math.random() * 10000}>{review}</li>
))}
</ul>
);
};
Lastly, here's my custom hook useSessionStorage:
export const useSessionStorage = <T>(
key: string,
initialValue?: T
): SessionStorage<T> => {
const [storedValue, setStoredValue] = useState<T | undefined>(() => {
if (!initialValue) return;
try {
const value = sessionStorage.getItem(key);
return value ? JSON.parse(value) : initialValue;
} catch (error) {
return initialValue;
}
});
useEffect(() => {
if (storedValue) {
try {
sessionStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.log(error);
}
}
}, [storedValue, key]);
return [storedValue, setStoredValue];
};
The title of my question says "how to rerender child component" because I noticed if I completely delete the ReviewList.tsx component, bringing all its render logic inside the ReviewForm.tsx, my application will behave as expected.
So maybe the problem is related with this relation between components?
Any advice is welcome.
The problem
The problem is in your useSessionStorage hook. It is not actually synchronized with the session storage, because the state is actually stored with useState, it is only populated on mount.
How does it work in your case:
You initialize FIRST STATE using useState (inside custom useSessionStorage hook) with current session storage value on component mount at ReviewList.tsx
You initialize SECOND STATE using useState (inside custom useSessionStorage hook) with current session storage value on component mount at ReviewForm.tsx
You mutate SECOND STATE and push the changes to session storage with useEffect
So FIRST STATE is not updated with the new value until you re-mount the component.
Solution 1 (Will work only for sync between different browser tabs)
We need to reverse the flow of data from useState -> sessionStorage to sessionStorage -> useState
export const useSessionStorage = <T>(
key: string,
initialValue?: T
): SessionStorage<T> => {
const [storedValue, setStoredValue] = useState<T | undefined>(() => {
if (!initialValue) return;
try {
const value = sessionStorage.getItem(key);
return value ? JSON.parse(value!) : initialValue;
} catch (error) {
return initialValue;
}
});
const setStorageValue = useCallback((newValue: T) => {
try {
sessionStorage.setItem(key, JSON.stringify(newValue));
} catch (error) {
console.log(error);
}
}, []);
/** This `useEffect` will make sure `storedValue` is always in sync with the `sessionStorage` */
useEffect(() => {
const listenToStorageEvent = (event: StorageEvent) => {
if (event.storageArea === sessionStorage && event.key === key) {
try {
const newValue = JSON.parse(event.newValue!);
if (storedValue !== newValue) {
setStoredValue(newValue);
}
} catch (error) {
console.log(error);
}
}
};
window.addEventListener("storage", listenToStorageEvent);
return () => {
window.removeEventListener("storage", listenToStorageEvent);
};
}, [key]);
// We expose `setStorageValue` which works with `sessionStorage` instead of `setStoredValue` which works with local state
return [storedValue, setStorageValue];
};
Solution 2
Use custom events to be able to sync the same tab too
https://github.com/imbhargav5/rooks/blob/main/src/hooks/useSessionstorageState.ts
Solution 3
Parse the whole session storage on application start and put it as a state into a context. After that, on each "set" update both the context state and the sessionStorage. This solution has a lot of disadvantages like error proneness due to manual state to session storage synchronization, excessive re-rendering of the whole component tree under session storage provider on each storage value update. So I will not even add code examples here.
Here I'm trying to reset selected radio buttons on this list,
however it doesn't work because
I previously change input check from {checked} to {user.checked}. Refer from UserListElement.tsx below
Therefore, I tried the following two methods.
in useEffect(), set user.userId = false
useEffect(() => {
user.checked = false;
}, [isReset, user]);
→ no change.
setChecked to true when addedUserIds includes user.userId
if (addedUserIds.includes(`${user.userId}`)) {
setChecked(true);
}
→ Unhandled Runtime Error
Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
Any suggestion on how to make this this work?
UserListElement.tsx
export const UserListElement = ({
user,
handleOnMemberClicked,
isReset,
}: {
user: UserEntity;
handleOnMemberClicked: (checked: boolean, userId: string | null) => void;
isReset: boolean;
}) => {
const [checked, setChecked] = useState(user.checked);
const addedUserIds = addedUserList.map((item) => item.userId) || [];
const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
const checkedState = e.target.checked;
setChecked(checkedState); //not called
user.checked = checkedState;
handleOnMemberClicked(checkedState, user.userId);
};
useEffect(() => {
setChecked(false);
}, [isReset, user]);
if (addedUserIds.includes(`${user.userId}`)) {
user.checked = true;
// setChecked(true) cause runtime error (infinite loop)
}
return (
<li>
<label className={style.checkboxLabel}>
<input
type="checkbox"
className={style.checkboxCircle}
checked={user.checked}
// checked={checked}
onChange={(e) => handleOnChange(e)}
/>
<span>{user.name}</span>
</label>
</li>
);
};
UserList.tsx
export const UserList = (props: {
showsUserList: boolean;handleClose: () => void;corporationId: string;currentUserId: string;samePerson: boolean;twj: string;
}) => {
const [isReset, setReset] = useState(false);
.......
const resetAll = () => {
setReset(!isReset);
setCount((addedUserList.length = 0));
setAddedUserList([]);
setUserName('');
};
......
return ( <
> < div > xxxxx <
ul className = {
`option-module-list no-list option-module-list-member ${style.personListMember}`
} > {searchedUserList.map((user, i) => (
<UserListElement user = { user }
handleOnMemberClicked = { handleOnMemberClicked }
isReset = { isReset }
key = {i} />
)) }
</ul>
/div>
<a className="is-secondary reservation-popup-filter-reset" onClick={resetAll}>
.....
}
UseAddUserList.tsx
export class UserDetail {
constructor(public userId: string | null, public name: string | null) {}
}
export let addedUserList: UserDetail[] = [];
export let setAddedUserList: Dispatch<SetStateAction<UserDetail[]>>;
export const useAddUserList = (idList: UserDetail[]) => {
[addedUserList, setAddedUserList] = useState(idList);
};
Further Clarification:
Default view
Searched option (showed filtered list)
I use user.checked because when using only checked, the checked state does not carry on from filtered list view to the full view (ex. when I erase searched word or close the popup).
The real answer to this question is that the state should NOT be held within your component. The state of checkboxes should be held in UsersList and be passed in as a prop.
export const UserListElement = ({
user,
handleOnMemberClicked,
isChecked
}: {
user: UserEntity;
handleOnMemberClicked: (checked: boolean, userId: string | null) => void;
isChecked: boolean;
}) => {
// no complicated logic in here, just render the checkbox according to the `isChecked` prop, and call the handler when clicked
}
in users list
return searchedUserList.map(user => (
<UserListElement
user={user}
key={user.id}
isChecked={addedUserIds.includes(user.id)} <-- THIS LINE
handleOnMemberClicked={handleOnMemberClicked}
/>
)
You can see that you almost had this figured out because you were doing this in the child:
if (addedUserIds.includes(`${user.userId}`)) {
user.checked = true;
// setChecked(true) cause runtime error (infinite loop)
}
Which indicates to you that the checkdd value is entirely dependent on the state held in the parent, which means there is actually no state to be had in the child.
Also, in React, NEVER mutate things (props or state) like - user.checked = true - that's a surefire way to leave you with a bug that will cost you a lot of time.
Hopefully this sheds some light
In your UserListElement.tsx you are setting state in render, which triggers renders the component again, and again set the state which again triggers re-render and the loop continues. Try to put your condition in the useEffect call, also you mutate props, so don't set user.checked = true. Instead call setter from the parent component, where it is defined.
useEffect(() => {
setChecked(false);
if (addedUserIds.includes(user.userId)) {
setChecked(true);
}
}, [user]);
I have a 3D globe that when you click on a country, returns an overlay of users.
I've previously had everything working just a file of randomised, mock user data in a local file. However I've now received the actual database (a AWS one) and I can't seem to get the fetch to return what I need. It should accept a signal from country click then return a list of users from this and show them in the pop up.
I've taken out the actual database for now (confidential) but this is working correctly on postman and a simplified fetch request I created. It doesn't seem to work within the wider context of this app.
At this stage, it doesn't break but it just returns an empty array in the console.
console.log output for this.state...
this is the state {overlay: true, users: Array(0), country: "Nigeria"}
import { onCountryClick } from "../Scene3D/AppSignals";
import OutsideClickHandler from "react-outside-click-handler";
import Users from "../Users";
import "./style.scss";
class Overlay extends Component {
constructor(props) {
super(props);
this.state = { overlay: false, users: [], country: [] };
this.openOverlay = this.openOverlay.bind(this);
this.closeOverlay = this.closeOverlay.bind(this);
this.fetchUsers = this.fetchUsers.bind(this);
}
openOverlay() {
this.setState({ overlay: true });
}
closeOverlay() {
this.setState({ overlay: false });
}
fetchUsers() {
fetch(
"**AWS***"
)
.then(response => response.json())
.then(data => this.setState({ users: data.users }));
}
onCountryClick = (country, users) => {
this.openOverlay();
this.fetchUsers();
this.setState({ country });
console.log("users", this.state.users);
};
componentDidMount() {
this.onCountrySignal = onCountryClick.add(this.onCountryClick);
}
componentWillUnmount() {
this.onCountrySignal.detach();
}
render() {
const country = this.state.country;
const users = this.state.users;
return (
<OutsideClickHandler
onOutsideClick={() => {
this.closeOverlay();
}}
>
<div className="users-container">
{this.state.overlay && (
<>
<h1>{country}</h1>
{users}
</>
)}
</div>
</OutsideClickHandler>
);
}
}
export default Overlay;```
[1]: https://i.stack.imgur.com/NWOlx.png
The problem is here
onCountryClick = (country, users) => {
this.openOverlay();
this.fetchUsers();
this.setState({ country });
console.log("users", this.state.users);
};
fetchUsers function is asynchronous and taken out from the original flow of execution, And the original execution stack continue to execute the console.log("users", this.state.users) which is empty at this time because fetchUsers function is not yet done executing. to prove it try to console log inside fetchUsers and you'll see who executed first.
My React app is supposed to display nearby shops to each user depending on their location.
What I'm trying to do is to get the latitude/longitude by window.geolocation in the componentDidMount() in which I'll call a REST API with the latitude/longitude as params to retrieve nearby shops.
I'm just trying to figure out how to organize and where to put the logic.
The idea is that you should try to use React's state before reaching for redux. It's both simpler and keeps as much of the state local as possible.
The React state stores the status of the API call (not made yet, success or failure) and the corresponding information about success (results) or failure (errorMessage). The important point is that your render function must explicitly handle all 3 cases.
class NearbyShops extends React.Component {
constructor(props) {
super(props);
this.state = {
queryStatus: 'initial',
results: [],
errorMessage: '',
}
}
async componentDidMount() {
try {
const results = await fetch('you.api.url/here');
this.setState(prevState => ({...prevState, results, queryStatus: 'success'}))
} catch (e) {
this.setState(prevState => ({...prevState, queryStatus: 'failure', errorMessage: e}))
}
}
render() {
const {results, queryStatus, errorMessage} = this.state;
if (queryStatus === 'success') {
return (
<div>{/* render results here */}</div>
)
} else if (queryStatus === 'failure') {
return (
<div>{errorMessage}</div>
)
} else {
return (
<div>Loading nearby shops...</div>
)
}
}
}