I have value X coming from the server. I would like to expose an interface similar to
interface Xclient {
getX(): Promise<X>
}
that I will later use from my react function component.
I say similar because behind the scenes I want it to:
first return value from the storage (its react-native)
simultaneously dispatch network call for newer version of X and re-render the component once I have the response
so instead of Promise I probably need Observable. But then how to use it with react in general and react hooks in particular?
I'm coming from the backend background so there may be some more canonical approach that I dont know about. I really dont want to use redux if possible!
If i understand correctly you have two data source (local storage and your api) and you want to get value from local storage and then get actual value from api. So, you should make next:
import { useState } from "react";
const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();
export function SimpleView() {
const [state, setState] = useState("default value");
localProvider()
.then((response) => {
setState(response);
apiProvider()
.then((actualResponse) => {
setState(actualResponse);
})
.catch(/* */);
})
.catch(/* */);
}
But I see no reason for call it synchronously and you can want to run it parallel:
import { useState } from "react";
const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();
export function SimpleView() {
const [state, setState] = useState("default value");
localProvider()
.then((response) => {
setState(response);
})
.catch(/* */);
apiProvider()
.then((actualResponse) => {
setState(actualResponse);
})
.catch(/* */);
}
If you want to encapsulate this logic you can make a function like this:
import { useState } from "react";
const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();
function getValue(localProvider, apiProvider, consumer) {
localProvider()
.then((response) => {
consumer(response);
})
.catch(/* */);
apiProvider()
.then((actualResponse) => {
consumer(actualResponse);
})
.catch(/* */);
}
export function SimpleView() {
const [state, setState] = useState("default value");
getValue(localProvider, apiProvider, setState);
}
UPD:
As #PatrickRoberts correctly noticed my examples contain the race condition, this code solves it:
import { useState } from "react";
const localProvider: Xclient = new Something();
const apiProvider: Xclient = new SomethingElse();
function getValue(localProvider, apiProvider, consumer) {
let wasApiResolved = false;
localProvider()
.then((response) => {
if (!wasApiResolved) {
consumer(response);
}
})
.catch(/* */);
apiProvider()
.then((actualResponse) => {
wasApiResolved = true;
consumer(actualResponse);
})
.catch(/* */);
}
export function SimpleView() {
const [state, setState] = useState("default value");
getValue(localProvider, apiProvider, setState);
}
I would personally write a custom hook for this to encapsulate React's useReducer().
This approach is inspired by the signature of the function Object.assign() because the sources rest parameter prioritizes later sources over earlier sources as each of the promises resolve:
import { useEffect, useReducer, useState } from 'react';
function useSources<T> (initialValue: T, ...sources: (() => Promise<T>)[]) {
const [fetches] = useState(sources);
const [state, dispatch] = useReducer(
(oldState: [T, number], newState: [T, number]) =>
oldState[1] < newState[1] ? newState : oldState,
[initialValue, -1]
);
useEffect(() => {
let mounted = true;
fetches.forEach(
(fetch, index) => fetch().then(value => {
if (mounted) dispatch([value, index]);
});
);
return () => { mounted = false; };
}, [fetches, dispatch]);
return state;
}
This automatically updates state to reference the result of the latest available source, as well as the index of the source which provided the value, each time a promise resolves.
The reason we include const [fetches] = useState(sources); is so the reference to the array fetches remains constant across re-renders of the component that makes the call to the useSources() hook.
That line could be changed to const fetches = useMemo(() => sources); if you don't mind the component potentially making more than one request to each source during its lifetime, because useMemo() doesn't semantically guarantee memoization. This is explained in the documentation:
You may rely on useMemo as a performance optimization, not as a semantic guarantee. In the future, React may choose to "forget" some previously memoized values and recalculate them on next render, e.g. to free memory for offscreen components.
Here's an example usage of the useSources() hook:
const storageClient: Xclient = new StorageXclient();
const networkClient: Xclient = new NetworkXclient();
export default () => {
const [x, xIndex] = useSources(
'default value',
() => storageClient.getX(),
() => networkClient.getX()
);
// xIndex indicates the index of the source from which x is currently loaded
// or -1 if x references the default value
};
Related
I created a hook to manipulate users data and one function is listener for users collection.
In hook I created subscriber function and inside that hook I unsubscribed from it using useEffect.
My question is is this good thing or maybe unsubscriber should be inside screen component?
Does my approach has cons?
export function useUser {
let subscriber = () => {};
React.useEffect(() => {
return () => {
subscriber();
};
}, []);
const listenUsersCollection = () => {
subscriber = firestore().collection('users').onSnapshot(res => {...})
}
}
In screen component I have:
...
const {listenUsersCollection} = useUser();
React.useEffect(() => {
listenUsersCollection();
}, []);
What if I, by mistake, call the listenUsersCollection twice or more? Rare scenario, but your subscriber will be lost and not unsubscribed.
But generally speaking - there is no need to run this useEffect with listenUsersCollection outside of the hook. You should move it away from the screen component. Component will be cleaner and less chances to get an error. Also, easier to reuse the hook.
I prefer exporting the actual loaded user data from hooks like that, without anything else.
Example, using firebase 9 modular SDK:
import { useEffect, useMemo, useState } from "react";
import { onSnapshot, collection, query } from "firebase/firestore";
import { db } from "../firebase";
const col = collection(db, "users");
export function useUsersData(queryParams) {
const [usersData, setUsersData] = useState(undefined);
const _q = useMemo(() => {
return query(col, ...(queryParams || []));
}, [queryParams])
useEffect(() => {
const unsubscribe = onSnapshot(_q, (snapshot) => {
// Or use another mapping function, classes, anything.
const users = snapshot.docs.map(x => ({
id: x.id,
...x.data()
}))
setUsersData(users);
});
return () => unsubscribe();
}, [_q]);
return usersData;
}
Usage:
// No params passed, load all the collection
const allUsers = useUsersData();
// If you want to pass a parameter that is not
// a primitive or a string
// memoize it!!!
const usersFilter = useMemo(() => {
return [
where("firstName", "==", "test"),
limit(3)
];
}, []);
const usersFiltered = useUsersData(usersFilter);
As you can see, all the loading and cleaning-up logic is inside the hook, and the component that uses this hook is as clear as possible.
I have a component in React that essentially autosaves form input 3 seconds after the user's last keystroke. There are possibly dozens of these components rendered on my webpage at a time.
I have tried using debounce from Lodash, but that did not seem to work (or I implemented it poorly). Instead, I am using a function that compares a local variable against a global variable to determine the most recent function call.
Curiously, this code seems to work in JSFiddle. However, it does not work on my Desktop.
Specifically, globalEditIndex seems to retain its older values even after the delay. As a result, if a user makes 5 keystrokes, the console.log statement runs 5 times instead of 1.
Could someone please help me figure out why?
import React, {useRef, useState} from "react";
import {connect} from "react-redux";
import {func1, func2} from "../actions";
// A component with some JSX form elements. This component shows up dozens of times on a single page
const MyComponent = (props) => {
// Used to store the form's state for Redux
const [formState, setFormState] = useState({});
// Global variable that keeps track of number of keystrokes
let globalEditIndex = 0;
// This function is called whenever a form input is changed (onchange)
const editHandler = (e) => {
setFormState({
...formState,
e.target.name: e.target.value,
});
autosaveHandler();
}
const autosaveHandler = () => {
globalEditIndex++;
let localEditIndex = globalEditIndex;
setTimeout(() => {
// Ideally, subsequent function calls increment globalEditIndex,
// causing earlier function calls to evaluate this expression as false.
if (localEditIndex === globalEditIndex) {
console.log("After save: " +globalEditIndex);
globalEditIndex = 0;
}
}, 3000);
}
return(
// JSX code here
)
}
const mapStateToProps = (state) => ({
prop1: state.prop1,
prop2: state.prop2
});
export default connect(
mapStateToProps, { func1, func2 }
)(MyComponent);
Note: I was typing up answer on how I solved this previously in my own projects before I read #DrewReese's comment - that seems like a way better implementation than what I did, and I will be using that myself going forward. Check out his answer here: https://stackoverflow.com/a/70270521/8690857
I think you hit it on the head in your question - you are probably trying to implement debounce wrong. You should debounce your formState value to whatever delay you want to put on autosaving (if I'm assuming the code correctly).
An example custom hook I've used in the past looks like this:
export const useDebounce = <T>(value: T, delay: number) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value]);
return debouncedValue;
};
// Without typings
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
};
Which you can then use like so:
const [myValue, setMyValue] = useState<number>(0);
const debouncedValue = useDebounce<number>(myValue, 3000);
useEffect(() => {
console.log("Debounced value: ", debouncedValue);
}, [debouncedFormState]);
// without typings
const [myValue, setMyValue] = useState(0);
const debouncedValue = useDebounce(myValue, 3000);
useEffect(() => {
console.log("Debounced value: ", debouncedValue);
}, [debouncedFormState]);
For demonstration purposes I've made a CodeSandbox demonstrating this useDebounce function with your forms example. You can view it here:
https://codesandbox.io/s/brave-wilson-cjl85v?file=/src/App.js
I have theorical question about custom hooks and use effect when redux is involved.
Let`s assume I have this code:
//MyComponent.ts
import * as React from 'react';
import { connect } from 'react-redux';
const MyComponentBase = ({fetchData, data}) => {
React.useEffect(() => {
fetchData();
}, [fetchData]);
return <div>{data?.name}</data>
}
const mapStateToProps= state => {
return {
data: dataSelectors.data(state)
}
}
const mapDispatchToProps= {
fetchData: dataActions.fetchData
}
export const MyComponent = connect(mapStateToProps, mapDispatchToProps)(MyComponentBase);
This works as expected, when the component renders it does an async request to the server to fetch the data (using redux-thunk). It initializes the state in the reduces, and rerender the component.
However we are in the middle of a migration to move this code to hooks. Se we refactor this code a little bit:
//MyHook.ts
import { useDispatch, useSelector } from 'react-redux';
import {fetchDataAction} from './actions.ts';
const dataState = (state) => state.data;
export const useDataSelectors = () => {
return useSelector(dataState);
}
export const useDataActions = () => {
const dispatch = useDispatch();
return {
fetchData: () => dispatch(fetchDataAction)
};
};
//MyComponent.ts
export const MyComponent = () => {
const data = useDataSelectors()>
const {fetchData} = useDataActions();
React.useEffect(() => {
fetchData()
}, [fetchData]);
return <div>{data?.name}</data>
}
With this change the component enters in an infite loop. When it renders for the first time, it fetches data. When the data arrives, it updates the store and rerender the component. However in this rerender, the useEffect says that the reference for fetchData has changed, and does the fetch again, causing an infinite loop.
But I don't understand why the reference it's different, that hooks are defined outside the scope of the component, they are not removed from the dom or whateverm so their references should keep the same on each render cycle. Any ideas?
useDataActions is a hook, but it is returning a new object instance all the time
return {
fetchData: () => dispatch(fetchDataAction)
};
Even though fetchData is most likely the same object, you are wrapping it in a new object.
You could useState or useMemo to handle this.
export const useDataActions = () => {
const dispatch = useDispatch();
const [dataActions, setDataActions] = useState({})
useEffect(() => {
setDataActions({
fetchData: () => dispatch(fetchDataAction)
})
}, [dispatch]);
return dataActions;
};
first of all if you want the problem goes away you have a few options:
make your fetchData function memoized using useCallback hook
don't use fetchData in your useEffect dependencies because you don't want it. you only need to call fetchData when the component mounts.
so here is the above changes:
1
export const useDataActions = () => {
const dispatch = useDispatch();
const fetchData = useCallback(() => dispatch(fetchDataAction), []);
return {
fetchData
};
};
the 2nd approach is:
export const MyComponent = () => {
const data = useDataSelectors()>
const {fetchData} = useDataActions();
React.useEffect(() => {
fetchData()
}, []);
return <div>{data?.name}</data>
}
I tried to dispatch the API call using redux in useEffect hooks. After the response came to redux-saga response goes to reducer and the reducer updated the state successfully but my component is not refreshing.
import React, { useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import SubscriptionComponent from '../../Components/Subscription/Subscription';
import SubscriptionActions from '../../Redux/Subscription/Actions';
import {
getMySubscriptions,
getMySubscriptionByName,
getMySubscriptionByGroup,
} from '../../Redux/Subscription/Selectors';
const Subscription = (props) => {
const { navigation } = { ...props };
const [visible, setVisible] = useState(false);
const subscriptionList = useSelector((state) => getMySubscriptions(state));
const dispatch = useDispatch();
const [data, setData] = useState(subscriptionList);
const payload = {
memberId: '604f2ad047bc495a0a7fad26',
vendorId: '5fd484c39590020dc0dfb82a',
vendorOrgId: '5fd484439590020dc0dfb829',
};
useEffect(() => {
dispatch(SubscriptionActions.fetchMySubscriptions(payload));
}, [data]);
const onHandleSubscriptionByName = () => {
setVisible(false);
const subscription = getMySubscriptionByName(data);
setData(subscription);
};
const onHandleSubscriptionByGroup = () => {
setVisible(false);
const subscription = getMySubscriptionByGroup(data);
setData(subscription);
};
return (
<SubscriptionComponent
list={data}
navigation={navigation}
onPressList={(val) =>
navigation.navigate('SubscriptionDetails', { _id: val._id, name: val.name })
}
visible={visible}
openMenu={() => setVisible(!visible)}
closeMenu={() => setVisible(!visible)}
sortByName={() => onHandleSubscriptionByName()}
sortBySub={() => onHandleSubscriptionByGroup()}
/>
);
};
export default Subscription;
used reselect to get the state from redux.
export const getMySubscriptions = createSelector(mySubscriptionSelector, (x) => {
const mySubscriptions = x.map((item) => {
return {
_id: item._id,
image: 'item.image,
description: item.description,
name: item.name,
subscriptionGroup: item.subscriptionGroup,
subscriptionAmount: item.subscriptionAmount,
status: item.status,
delivery: item.delivery,
product: item.product,
};
});
return mySubscriptions ;
});
Why component is not refreshing.
Thanks!!!
You're storing the selection result in local state.
const subscriptionList = useSelector((state) => getMySubscriptions(state));
const [data, setData] = useState(subscriptionList);
useState(subscriptionList) will only set data initially not on every update.
EDIT:
Your setup is a little odd:
useEffect(() => {
dispatch(SubscriptionActions.fetchMySubscriptions(payload));
}, [data]);
Using data in the dependency array of useEffect, will cause refetching the data, whenever data is updated. Why? I looks like your sorting is working locally, so no need to refetch?
I would suggest to store the sort criteria (byName, byGroup) also in Redux and eliminate local component state, like that:
// ToDo: rewrite getMySubscriptions so that it considers sortCriteria from Redux State
const subscriptionList = useSelector(getMySubscriptions);
const dispatch = useDispatch();
};
useEffect(() => {
dispatch(SubscriptionActions.fetchMySubscriptions(payload));
// Empty dependency array, so we're only fetching data once when component is mounted
}, []);
const onHandleSubscriptionByName = () => {
dispatch(SubscriptionActions.setSortCriteria('byName'));
};
const onHandleSubscriptionByGroup = () => {
dispatch(SubscriptionActions.setSortCriteria('byGroup'));
};
As mentioned in the comments you will need to add a new action setSortCriteria and reducer to handle the sorting and adjust your selector, so that it filters the subscription list when a sortCriteria is active.
You do not update data after fetching new subscription.
const [data, setData] = useState(subscriptionList);
Only initializes data, but does not update it, you need to add useEffect to update data:
useEffect(() => {
setData(subscriptionList);
}, [JSON.stringify(subscriptionList)]);
JSON.stringify only used for deep compare complex objects, since useEffect only runs shallow compare and might miss, changes in objects.
-----EDIT------
Other problem might be that your getMySubscriptions function might need deep compare, since useSelector by itself doesn't do that, example might be:
import { useSelector, shallowEqual } from 'react-redux';
const subscriptionList = useSelector((state) => getMySubscriptions(state), shallowEqual);
Note that both solutions must be used.
I have the following functional component. I did debugging and read some posts about react-redux and useEffect, but still have had no success. On initial render the state in my redux store is null but then changes to reflect new state with data. However, my react UI does not reflect this. I understand what the issue is, but I don't know exactly how to fix it. I could be doing things the wrong way as far as getting the data from my updated state in the redux store.
Here is my component :
const Games = (props) => {
const [gamesData, setGamesData] = useState(null)
const [gameData, setGameData] = useState(null)
const [gameDate, setGameDate] = useState(new Date(2020, 2, 10))
const classes = GamesStyles()
// infinite render if placed in
// useEffect array
const {gamesProp} = props
useEffect(() => {
function requestGames() {
var date = parseDate(gameDate)
try {
props.getGames(`${date}`)
// prints null, even though state has changed
console.log(props.gamesProp)
setGamesData(props.gamesProp)
} catch (error) {
console.log(error)
}
}
requestGames()
}, [gameDate])
// data has not been loaded yet
if (gamesData == null) {
return (
<div>
<Spinner />
</div>
)
} else {
console.log(gamesData)
return (
<div><p>Data has been loaded<p><div>
{/* this is where i would change gameDate */}
)
}
}
const mapStateToProps = (state) => {
return {
gamesProp: state.gamesReducer.games,
}
}
const mapDispatchToProps = (dispatch) => {
return {
getGames: (url) => dispatch(actions.getGames(url)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Games)
Here is my reducer
import {GET_GAMES} from '../actions/types'
const initialState = {
games: null // what we're fetching from backend
}
export default function(state = initialState, action){
switch(action.type){
case GET_GAMES:
// prints correct data from state
//console.log(action.payload)
return{
...state,
games: action.payload
}
default:
return state
}
}
Here is my action
import axios from 'axios'
import {GET_GAMES} from './types'
// all our request go here
// GET GAMES
export const getGames = (date) => dispatch => {
//console.log('Date', date)
axios.get(`http://127.0.0.1:8000/games/${date}`)
.then(res => {
dispatch({
type: GET_GAMES,
payload: res.data
})
}).catch(err => console.log(err))
}
When I place the props from state in my dependencies array for useEffect, the state updates but results in an infinite render because the props are changing.
This happens even if I destruct props.
Here is an image of my redux state after it is updated on the initial render.
You were running into issues because you were trying to set the state based off of data that was in a closure. The props.gamesProp within the useEffect you had would never update even when the parent data changed.
The reason why props.gamesProp was null in the effect is because in each render, your component essentially has a new instance of props, so when the useEffect runs, the version of props that the inner part of the useEffect sees is whatever existed at that render.
Any function inside a component, including event handlers and effects, “sees” the props and state from the render it was created in.
https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function
Unless you have to modify gamesState within your component, I highly recommend that you don't duplicate the prop to the state.
I'd also recommend using useDispatch and useSelector instead of connect for function components.
Here's some modifications to your component based on what I see in it currently and what I've just described:
import { useDispatch, useSelector } from 'react-redux';
const Games = (props) => {
const gamesData = useSelector((state) => state.gamesReducer.games);
const dispatch = useDispatch();
const [gameData, setGameData] = useState(null);
const [gameDate, setGameDate] = useState(new Date(2020, 2, 10));
const classes = GamesStyles();
// infinite render if placed in
// useEffect array
useEffect(() => {
const date = parseDate(gameDate);
try {
dispatch(actions.getGames(`${date}`));
} catch (error) {
console.log(error);
}
}, [gameDate, dispatch]);
// data has not been loaded yet
if (gamesData == null) {
return (
<div>
<Spinner />
</div>
);
} else {
console.log(gamesData);
return (
<div>
<p>Data has been loaded</p>
</div>
// this is where i would change gameDate
);
}
};
export default Games;
If you need to derive your state from your props, here's what the React Documentation on hooks has to say:
https://reactjs.org/docs/hooks-faq.html#how-do-i-implement-getderivedstatefromprops