React JS & Recoil set/get on select/selectFamily for specific attribute - reactjs

I use Recoil state management in ReactJS to preserve a keyboard letters data, for example
lettersAtom = atom(
key: 'Letters'
default: {
allowed : ['A','C','D']
pressedCounter : {'A':2, 'D':5}
}
)
lettersPressedSelect = selector({
key: 'LettersPressed',
get: ({ get }) => get(lettersAtom).pressedCounter, //Not work, returns undefined
set: () => ({ set }, pressedLetter) => {
let newState = {...lettersAtom};
newState.pressedCounter[pressedLetter]++;
set(lettersAtom, newState);
}
}),
In functional component i use
const [letters,setLetters] = useRecoilState(lettersAtom);
const [pressedCounter, setPressedCounter] = useRecoilState(lettersPressedSelect);
each time the a keyboard letter pressed the pressedCounter I want to increased for corresponded letter like that
setPressedCounter('A');
setPressedCounter('C'); ///etc...
How to achieve that ? Does recoil have a way to get/set a specific part/sub of json attribute ? (without make another atom? - I want to keep "Single source of truth")
Or do you have a suggetion better best practice to do that ?

There are some bugs in your code: no const, braces in atom call and no get inside the set. You also need spread the pressedCounter.
Overwise your solution works fine.
In Recoil you update the whole atom. So in this particular case you probably don't need the selector. Here is a working example with both approaches:
https://codesandbox.io/s/modest-wind-kosp7o?file=/src/App.js
It a best-practice to keep atom values rather simple.

You can update the state based on the existing state in a selector in a couple ways. You could use the get() callback from the setter or you could use the updater form of the setter where you pass a function as the new value which receives the current value as a parameter.
However, it's a good practice to have symmetry for the getter and setters of a selector. For example, here's a selector family which gets and sets the value of a counter:
const lettersPressedState = selectorFamily({
key: 'LettersPressed',
get: letter => ({ get }) => get(lettersAtom).pressedCounter[letter],
set: letter => ({ set }, newPressedValue) => {
set(lettersAtom, existingLetters => ({
...existingLetters,
pressedCounter: {
...existingLetters.pressedCounter,
[letter]: newPressedValue,
},
});
},
});
But note that the above will set the new value with a new counter value where you originally wanted the setter to increment the value. That's not really setting a new value and is more like an action. For that you don't really need a selector abstraction at all and can just use an updater when setting the atom:
const [letters, setLetters] = useRecoilState(lettersAtom);
const incrementCounter = pressedLetter =>
setLetters(existingLetters => ({
...existingLetters,
pressedCounter: {
...existingLetters.pressedCounter,
[pressedLetter]: (existingLetters.pressedCounter[pressedLetter] ?? 0) + 1,
},
});
Note that this uses the updater form of the selector to ensure it is incrementing based on the current value and not a potentially stale value as of the rendering.
Or, you can potentially simplify things more and use simpler values in the atoms by using an atom family for the pressed counter:
const pressedState = atomFamily({
key: 'LettersPressed',
default: 0,
});
And you can update in your component like the following:
const [counter, setCounter] = useRecoilState(pressedState(letter));
const incrementCounter = setCounter(x => x + 1);
Or create an general incrementor callback:
const incrementCounter = useRecoilCallback(({ set }) => pressedLetter => {
set(pressedState(pressedLetter)), x => x + 1 );
});

So the sort answer help by user4980215 is:
set: () => ({ get, set }, pressedLetter) => {
let newState = {...get(lettersAtom)};
newState.pressedCounter[pressedLetter]++;
set(lettersAtom, newState);
}

Related

Recoil atom/atomFamily deep comparison

Out of the box recoil will trigger updates after a shallow comparison when setting atom state. Is there a way to have the atom or atomFamily perform a deep comparison instead?
A workaround is to have an atom + selector factory, where the set function of the selector performs a deep comparison (see below). This works well, but has the limitation of not being usable within atomic Recoil transactions. (workaround credit to a github thread I can't seem to find)
export const createAtomWithDeepEqualityCheck = <T>(options: AtomOptions<T>) => {
const _atom = atom(options);
const wrappedAtom = selector<T>({
key: `selector-${options.key}`,
cachePolicy_UNSTABLE: { eviction: 'most-recent' },
get: ({ get }) => get(_atom),
set: ({ get, set }, newValue) => {
const currValue = get(_atom);
if (_isEqual(currValue, newValue)) {
return;
}
set(_atom, newValue);
},
});
return wrappedAtom;
};

ReactJS, update State with handler but data is empty

I have this situation:
I want to update some state (an array) which is used to map different React components.
Those componentes, have their own handleUpdate.
But when I call to handleUpdate the state that I need to use is empty. I think is because each handler method was mounted before the state was filled with data, but then, how could I ensure or use the data in the handler? In other words, the handler needs to update the state that fill it's own state:
const [data, setData] = useState([]);
const [deliver, setDeliver] = useState({items: []});
const handleUpdate = (value, position) => {
// This set works
setDeliver({
items: newItems
});
// This doesn't work because "data" is an empty array - CRASH
setData(data[position] = value);
};
useEffect(() => {
const dataWithComponent = originalData.map((item, i) => ({
...item,
entregado: <SelectorComponent
value={deliver?.items[i].delivered}
key={i}
onUpdate={(value) => handleUpdate(value, i)}
/>
}));
setData(dataWithComponent); // This is set after <SelectComponent is created...
}
}, [originalData]);
The value that you pass don't come from originalData, so the onUpdated don't know what it's value
You run on originalData using map, so you need to pass item.somthing the the onUpdate function
const dataWithComponent = originalData.map((item, i) => ({
...item,
entregado: <SelectorComponent
value={deliver?.items[i].delivered} // you can't use items use deliver.length > 0 ? [i].delivered : ""
key={i}
onUpdate={() => handleUpdate("here you pass item", i)}
/>
}));
I'm not sure, but I think you can do something like that. I hope you get some idea to work it.
// This doesn't work because "data" is an empty array - CRASH
let tempData = [...data].
tempData[position] = value;
setData(tempData);

React Hooks - keep arguments reference in state

I created a hook to use a confirm dialog, this hook provides the properties to the component to use them like this:
const { setIsDialogOpen, dialogProps } = useConfirmDialog({
title: "Are you sure you want to delete this group?",
text: "This process is not reversible.",
buttons: {
confirm: {
onPress: onDeleteGroup,
},
},
width: "360px",
});
<ConfirmDialog {...dialogProps} />
This works fine, but also I want to give the option to change these properties whenever is needed without declaring extra states in the component where is used and in order to achieve this what I did was to save these properties in a state inside the hook and this way provide another function to change them if needed before showing the dialog:
interface IState {
isDialogOpen: boolean;
dialogProps: TDialogProps;
}
export const useConfirmDialog = (props?: TDialogProps) => {
const [state, setState] = useState<IState>({
isDialogOpen: false,
dialogProps: {
...props,
},
});
const setIsDialogOpen = (isOpen = true) => {
setState((prevState) => ({
...prevState,
isDialogOpen: isOpen,
}));
};
// Change dialog props optionally before showing it
const showConfirmDialog = (dialogProps?: TDialogProps) => {
if (dialogProps) {
const updatedProps = { ...state.dialogProps, ...dialogProps };
setState((prevState) => ({
...prevState,
dialogProps: updatedProps,
}));
}
setIsDialogOpen(true);
};
return {
setIsDialogOpen,
showConfirmDialog,
dialogProps: {
isOpen: state.isDialogOpen,
onClose: () => setIsDialogOpen(false),
...state.dialogProps,
},
};
};
But the problem here is the following:
Arguments are passed by reference so if I pass a function to the button (i.e onDeleteGroup) i will keep the function updated to its latest state to perform the correct deletion if a group id changes inside of it.
But as I'm saving the properties inside a state the reference is lost and now I only have the function with the state which it was declared at the beginning.
I tried to add an useEffect to update the hook state when arguments change but this is causing an infinite re render:
useEffect(() => {
setState((prevState) => ({
...prevState,
dialogProps: props || {},
}));
}, [props]);
I know I can call showConfirmDialog and pass the function to update the state with the latest function state but I'm looking for a way to just call the hook, declare the props and not touch the dialog props if isn't needed.
Any answer is welcome, thank you for reading.
You should really consider not doing this, this is not a good coding pattern, this unnecessarily complicates your hook and can cause hard to debug problems. Also this goes against the "single source of truth" principle. I mean a situation like the following
const Component = ({title}: {title?: string}) => {
const {showConfirmDialog} = useConfirmDialog({
title,
// ...
})
useEffect(() => {
// Here you expect the title to be "title"
if(something) showConfirmDialog()
}, [])
useEffect(() => {
// Here you expect the title to be "Foo bar?"
if(somethingElse) showConfirmDialog({title: 'Foo bar?'})
}, [])
// But if the second dialog is opened, then the first, the title will be
// "Foo bar?" in both cases
}
So please think twice before implementing this, sometimes it's better to write a little more code but it will save you a lot debugging.
As for the answer, I would store the props in a ref and update them on every render somehow like this
/** Assign properties from obj2 to obj1 that are not already equal */
const assignChanged = <T extends Record<string, unknown>>(obj1: T, obj2: Partial<T>, deleteExcess = true): T => {
if(obj1 === obj2) return obj1
const result = {...obj1}
Object.keys(obj2).forEach(key => {
if(obj1[key] !== obj2[key]) {
result[key] = obj2[key]
}
})
if(deleteExcess) {
// Remove properties that are not present on obj2 but present on obj1
Object.keys(obj1).forEach(key => {
if(!obj2.hasOwnProperty(key)) delete result[key]
})
}
return result
}
const useConfirmDialog = (props) => {
const localProps = useRef(props)
localProps.current = assignChanged(localProps.current, props)
const showConfirmDialog = (changedProps?: Partial<TDialogProps>) => {
localProps.current = assignChanged(localProps.current, changedProps, false)
// ...
}
// ...
}
This is in case you have some optional properties in TDialogProps and you want to accept Partial properties in showConfirmDialog. If this is not the case, you could simplify the logic a little by removing this deleteExcess part.
You see that it greatly complicates your code, and adds a performance overhead (although it's insignificant, considering you only have 4-5 fields in your dialog props), so I really recommend against doing this and just letting the caller of useConfirmDialog have its own state that it can change. Or maybe you could remove props from useConfirmDialog in the first place and force the user to always pass them to showConfirmDialog, although in this case this hook becomes kinda useless. Maybe you don't need this hook at all, if it only contains the logic that you have actually shown in the answer? It seems like pretty much the only thing it does is setting isDialogOpen to true/false. Whatever, it's your choice, but I think it's not the best idea

Rendering and best practices in React

I am a beginner in React, I have a question regarding rendering, which consists of I use useEfect to render the change in a variable, which is decorated in a useState
Here are the statements:
const CardRepo: React.FC<DadosCardRepo> = ({ Nome }) => {
const [nomeRepo, setNomeRepo] = useState(Nome);
const [data, setData] = useState({});
const [languages, setLanguages] = useState<Languages[]>([]);
This is a component, which receives an object with a Name, and sets this value Name in nameRepo
Here are the two methods:
useEffect(() => {
getRepo(nomeRepo)
.then(data => {
setData(data);
});
}, [nomeRepo]);
useEffect(() => {
getLanguages(data['languages_url'])
.then(data => {
let total = 0;
const languagesRef: Languages[] = [];
Object.keys(data).map((key) => {
total += data[key];
languagesRef.push({ Nome: key, Porct: data[key] });
});
languagesRef.map((language, i, array) => {
setLanguages((oldValues) => [...oldValues, { Nome: language.Nome.toLowerCase(), Porct: +((language.Porct * 100) / total).toFixed(2) }]);
});
});
}, [data]);
Where the first useEfect I run when making a change to nomeRepo, and in this method, I make a request to the getRepo service
And the second one, when I change the date, and in this method, I make a request to the getLanguages ​​service, and do the processing of the response to make a list with it, where I assign languages.
Here is the listing:
{
languages.map((el, i) => {
return (
<div className="progress-bar" id={el.Nome} key={i} style={{ width: el.Porct + '%' }}></div>
);
})
}
My question is related to rendering, I know that I will only be able to list something, if I use useEfect in the variable "languages", where I will be assigning values ​​and with the changes in the variable to render again, otherwise I couldn't.
But when I make any changes to the code, it renders only where it was changed, but it assigns the same values ​​that were assigned, for example: [1,2,3] => [1,2,3,1,2,3].
I tried to put setLanguages ​​([]), but it has the same behavior
I was wondering how to fix this and if it's a good practice to make calls and list that way.
first, there is one bad practice where you create a derived state from props. nomeRepo comes from props Nome, you should avoid this derived states the most of you can. You better remove nomeRepo state and use only Nome.
your first use Effect becomes:
useEffect(() => {
getRepo(Nome)
.then(data => {
setData(data);
});
}, [Nome]);
you second useEffect has a setState triggered inside a map. That will trigger your setState multiple times, creating several rerenders. It's best to prepare your array of languages first, then call setState once.
Also, you should use forEach instead of map if you are not consuming the returned array, and only running some script on each element.
One thing to notice, is you are adding new languages to old ones. I assume that languages should be related data['languages_url'] value, this way you wouldn't include the old languages to the setLanguages.
One thing you could consider is to keep your total values instead of percentages. you can store total into a variable with useMemo, that will depend on languages. And declare a your percentage function into a utils folder and import it as need it.
refactored second useEffect:
// calculate total value that gets memoized
const totalLangCount = useMemo(() => languages.reduce((total, val) => total + val.count , 0), [languages])
useEffect(() => {
getLanguages(data['languages_url'])
.then(data => {
const languagesRef: Languages[] = [];
Object.keys(data).forEach((key) => {
// prepeare your languages properties
const nome = key.toLowerCase();
const count = data[key];
languagesRef.push({ nome, count });
});
// avoid multiple setLanguages calls
setLanguages(languagesRef)
});
}, [data]);
languages rendered:
{
languages.map((el, i) => {
return (
// here we are using array's index as key, but it's best to use some unique key overall instead
<div className="progress-bar" id={el.nome} key={i} style={{ width: percentage(el.count, total) }}></div>
);
})
}
percentage function reusable:
// utils percentage exported
export const percentage = (count, total) => ((count * 100) / total).toFixed(2) + '%'

How to properly memoize mapDispatchToProps?

First, here's my HOC:
export default function connectField({
nameProp = 'name',
valueProp = 'value',
dispatchProp = 'dispatch'
}: ConnectOptions) {
return compose(
getContext(contextTypes),
connect((state, ownProps) => {
const path = [namespace,...getPath(ownProps),...toPath(ownProps[nameProp])];
const value = getOr('', path, state);
return {
[valueProp]: value
};
}, (dispatch,ownProps) => { // <----------- mapDispatchToProps
const path = [...getPath(ownProps),...toPath(ownProps[nameProp])];
return {
[dispatchProp]: value => dispatch({type: ActionTypes.Change, payload: {path, value}})
};
}, (stateProps, dispatchProps, {[FIELD_PATH]: _, ...ownProps}) => {
return {...stateProps, ...dispatchProps, ...ownProps};
}, {
areMergedPropsEqual: (a,b) => {
let eq = shallowEqual(a,b);
console.log('areMergedPropsEqual',a,b,eq);
return eq;
},
}),
withContext(contextTypes, props => {
return {[FIELD_PATH]: [...getPath(props), props[nameProp]]};
}),
);
}
In the middle there is my mapDispatchToProps. That's causing areMergedPropsEqual to return false every time because it's creating a new action creator every time.
I can't figure out how to memoize this bit:
value => dispatch({type: ActionTypes.Change, payload: {path, value}})
Such that I get back the same function instance every time.
There's some notes in the docs about "per-instance memoization" which is what I want, but I can't quite make heads or tails of what I'm supposed to do here.
To be clear, I know how to memoize a function. However, I don't want a use a big cache with infinite history. It's unnecessary memory consumption. I just need a cache size of 1 like how reselect does it. The problem is that I can't create the "selector" directly inside connectField because that still creates a single shared instance -- i.e., all "connected fields" will share the same cache and they'll overwrite each other, negating the benefit. It has to be per component instance. This is specific to React-Redux's connect method. There's a syntax for it so that you can create your selector at the right spot, and it will only get ran once per instance. I'm just having trouble deciphering the API -- do they expect a function that returns a function that returns an object? Or an object with propnames as keys and functions as values? What does that function return? i.e., the docs aren't clear about all the different variations that are accepted for the mapDispatchToProps option.
If you already have lodash, you have a memoize function that allow to transform any function into a memoized function. This memoized function will calculate the return value for a given parameter and will then always return this same return value each you supply the same parameter.
You can use it like this for example :
import { memoize } from 'lodash'
const makeChangeDispatcher = memoize( (dispatch, path) =>
value => dispatch({type: ActionTypes.Change, payload: {path, value}})
)
...
(dispatch,ownProps) => { // <----------- mapDispatchToProps
const path = [...getPath(ownProps),...toPath(ownProps[nameProp])];
return {
[dispatchProp]: makeChangeDispatcher(dispatch, path)
};
}
...
You can see more infos on the lodash documentation of memoize

Resources