So I have seen one useful way of using scan where you map a function to the stream to update an initial value.
const initialState = false;
const notThis = (x) => !x;
const toggle = clicks.map(()=> notThis)
.startWith(initialState)
.scan((acc,curr)=> curr(acc))
But if I need the values from clicks I know I can write a partial
const initialState = {klass:'',bool:false};
const toggle = clicks
.map((y)=> {
return (x) => {
let klass = y.target.className;
let bool = !x.bool;
return ({klass, bool});
};
})
.startWith(initialState)
.scan((acc,curr)=> curr(acc));
This could be very useful if I am merging several streams, but it also seem like it could be overly complicated. Is there a better way to accomplish passing data and functions down the stream? Here is a bin of this example link
Well, you should ask yourself if you need to pass down functions. I made your code a bit simpler:
https://jsbin.com/timogitafo/1/edit?js,console,output
Or an even simpler one: https://jsbin.com/giqoduqowi/1/edit?js,console,output
Related
I've encountered interesting problem with custom react hooks, rule of hooks, and recursion. I would like to know if I'm doing something wrong. Thanks for you time.
I have a custom hook useServerData(entityIds). This hook internaly calls (depends on) other custom hook useFilters(entityIds). This hook cyclickly depends on useServerData(shorterEntityIds). Maybe it seems like a logical defect, but it is just a recurion because shorterEntityIds are strictly shorter array. Without rule of hooks, I would wrote recursion break like:
let filters = [];
if (shorterEntityIds.length > 0) {
filters = useFilters(shorterEntityIds)
}
But it is prohibited by rule of hooks (and I understand why). Without the break JavaScript throws Maximum call stack size exceeded.
How would you solve this? How to break the cycle? What is a React idiomatic way?
Thank you.
EDIT: As Bergi requested I'm adding more specific code for this.
// Returns dictionary where key is given filterId and value is QueryResult<FilterRange>
// Where returned FilterRange depends on Filter and prescending Filters for each Filter
export const useFilterRanges = (filterIds) => {
const filtersById = useFilters(filterIds);
const prescendingQueryFiltersById = usePrescendingQueryFilters(filterIds);
const filterRanges = useQueries(filterIds.map((filterId) => {
const filter = filtersById[filterId];
const prescendingFilters = prescendingQueryFiltersById[filterId];
return fetchFilterRange(filter, prescendingFilters);
}));
return zipObject(filterIds, filterRanges);
};
// Returns dictionary where key is given filterId and value is Array<QueryFilter>
// Where returned Array<QueryFilter> depends on internal state (order of Filters) and QueryFilters
export const usePrescendingQueryFilters = (filterIds) => {
const allFilterIds = useAllFilterIds();
const index = findLastIndex(allFilterIds, (filterId) => includes(filterIds, filterId));
// allPrecendingIds are always at least one item shorter than filterIds
const allPrecendingIds = take(allFilterIds, index);
const queryFiltersById = useQueryFilters(allPrecendingIds);
return chain(filterIds)
.keyBy()
.mapValues((filterId) => {
const index = indexOf(allFilterIds, filterId);
const precendingIds = take(allFilterIds, index);
const queryFilters = precendingIds.map((id) => queryFiltersById[id]);
return queryFilters.flatMap((queryFilter) => queryFilter ?? []);
})
.value();
};
// Returns dictionary where key is given filterId and value is QueryFilter
// Where returned QueryFilter depends on QueryResult<FilterRange> and Filter
export const useQueryFilters = (filterIds) => {
const filtersById = useFilters(filterIds);
const rangesById = useFilterRanges(filterIds);
return chain(filterIds)
.keyBy()
.mapValues((filterId) => {
const filter = filtersById[filterId];
const range = rangesById[filterId];
return constructQueryFilters(filter, range);
})
.value();
};
Note the code is simplified (I've doublechecked but it can contain some typos) and the real project cycle is even bigger, but I believe this is kind of minimal meaningful example. :)
There's a commonly used utility hook "useLatest", which returns a ref containing the latest value of the input. There are 2 common implementations:
const useLatest = <T>(value: T): { readonly current: T } => {
const ref = useRef(value);
ref.current = value;
return ref;
};
From https://github.com/streamich/react-use/blob/master/src/useLatest.ts
const useLatest = <T extends any>(current: T) => {
const storedValue = React.useRef(current)
React.useEffect(() => {
storedValue.current = current
})
return storedValue
}
From https://github.com/jaredLunde/react-hook/blob/master/packages/latest/src/index.tsx
The first version isn't suitable for React 18's concurrent mode, the second version will return the old value if used before useEffect runs (e.g. during render).
Is there a way to implement this that's both concurrent-safe and consistently returns the correct value?
Here's my attempt:
function useLatest<T>(val: T): React.MutableRefObject<T> {
const ref = useRef({
tempVal: val,
committedVal: val,
updateCount: 0,
});
ref.current.tempVal = val;
const startingUpdateCount = ref.current.updateCount;
useLayoutEffect(() => {
ref.current.committedVal = ref.current.tempVal;
ref.current.updateCount++;
});
return {
get current() {
// tempVal is from new render, committedVal is from old render.
return ref.current.updateCount === startingUpdateCount
? ref.current.tempVal
: ref.current.committedVal;
},
set current(newVal: T) {
ref.current.tempVal = newVal;
},
};
}
This hasn't been thoroughly tested, just wrote it while writing this question, but it seems to work most of the time. It should be better than both versions above, but it has 2 issues: it returns a different object every time and it's still possible to be inconsistent in this scenario:
Render 1:
ref1 = useLatest(val1)
Create function1, which references ref1
Commit (useLayoutEffect runs)
Render 2:
useLatest(val2)
Call function1
function1 will use val1, but it should use val2.
Here is what I think is correct:
const useLatest = <T extends any>(current: T) => {
const storedValue = React.useRef(current)
React.useLayoutEffect(() => {
storedValue.current = current
})
return storedValue.current
}
Is there a way to implement this that's both concurrent-safe and consistently returns the correct value?
The question doesn't actually explain what "this" means, i.e. how is useLatest called, and what purpose it fulfills in the application. So I'll have to guess for that ;) A somewhat realistic example would be very helpful.
In any case, it's probably useful to take a step back and ask if useLatest is the most suitable solution. If you find you don't need it, you also won't have to fix it.
With the way it works (depending on an effect to capture the value), it indeed won't play well with concurrent features. But even without them, it's an unreliable approach as the ref theoretically can change at any point, making renders unpredictable.
My guess of the use case is something similar to the proposed (and partially accepted) useEvent hook (GitHub PR).
function Chat() {
const [text, setText] = useState('');
const onClick = useEvent(() => {
sendMessage(text);
});
return <SendButton onClick={onClick} />;
}
Its purpose is to capture the latest render's scope, like useCallback, but without the need for dependencies. It does this by using an unchanging callback that internally calls the latest created callback, in a ref that is re-assigned on every render. That way passing that callback as a prop won't cause any renders by itself.
You can implement this yourself already, but the RFC mentions some open questions about this approach.
export function useEvent(handler) {
const latestHandlerRef = useRef();
useLayoutEffect(() => {
latestHandlerRef.current = handler;
});
// Never changing callback.
return useCallback((...args) => {
latestHandlerRef.current(...args)
}, []);
}
I also tested setting latestHandlerRef.current = handler directly in render instead of the layout effect. For now this seems to work as expected but that's just my use case. In the PR some doubt is expressed over assigning to a ref during render, though possibly these concerns don't really apply here, as the ref is only ever accessed in the callback.
I'm having trouble thinking of the best way to go about this without a framework. Let's say I have code that looks like this.
let x = {
someData: 5
}
let config = [{
moreData: `You have ${x.someData} flunderflaffles`
}]
//user input affects x
button.onclick = ()=>{
x.someData++
appendEle()
}
const appendEle = () => {
document.body.append(`<p>${config.moreData}</p>`)
}
This is a very simple explanation of what I'm doing right now but I hope you get the gist. Basically config.moreData will always have a value of 5 since thats what it was when it was initialized. Can anyone shed some light on how to handle this better so that the data in config reflects that in x? Thank you.
You have to ensure that the string is created dynamically, when you need it. For that you need a function. A getter property enables the API to remain the same:
const config = {
get moreData() { return `You have ${x.someData} flunderflaffles` }
}
Usage:
const moreData = config.moreData
Suppose I have a function GetUserRecommendedSongs. It does the following:
It presents the user with a dialog: what is your mood today?
(happy\gloomy\nostalgic)
Using the result it calls a service GetUserRecommendationsByMood.
It then returns a result for example: {mood: "nostalgic", songIds:
[12, 25]};
The caller of this function (possibly several ui components) would
use the result to play the songs under the title "so you feel ${result.mood} today?"
The problem is I use the mood twice: to get the recommendations, and in the final result.
With async\await I would do:
const requiredMood = await ShowRequiredMoodDialog();
//handle cancellation e.g. if(!mood)
let recommendedSongs = await GetUserRecommendations(mood);
return {mood, recommendedSongs};
However with rxjs I was only able to come up with the following:
let mood$ = ShowRequiredMoodDialog().pipe(share) //has to be shared so we don't show the dialog twice
let recommendedSongs$ = mood$.pipe(switchMap((mood)=> GetUserRecommendations(mood)));
return forkJoin(mood$, recommendedSongs$) //with some selector\map to turn into object
(note to reader: don't use this as a reference to rxjs, as I did not test this code)
This code is quite complicated. Can it be simplified?
The only different way that comes to my mind is like this but I don't know which one is more readable:
mood$.pipe(
switchMap(mood => GetUserRecommendations(mood).pipe(
map(recommendedSongs => [mood, recommendedSongs]),
),
)
What about this one?
let mood$ = ShowRequiredMoodDialog().pipe(share());
let recommend = (mood) => {
return { mood, songs: GetUserRecommendations(mood) };
};
let recommendedSongs$ = mood$.pipe(switchMap(recommend));
return recommendedSongs$;
I have the following code:
ngOnInit(): void
{
const source = this.categoryService.getCategories();
console.log(source instanceof Observable);
const example = source.map((categor) => categor.map((categories) => {
const links = this.categoryService.countCategoryLinks(categories.id);
const aff = example.merge(links).subscribe(linke => console.log(linke));
return categories.id
}));
}
where getCategories() returns an observable.
On each item of this Observable, I get categories.id field to run another method called countCategoryLinks(categories.id) that also returns an Observable().
My problem is that : I only have 3 categories (ok), the countCategoryLinks() returns 3 items (ok) but the code above shows an infinite loop in the console.
Both methods started "manually" for testing purpose do not show any loop.
The problem really comes from the code above.
Any idea ?
Thanks in advance and Regards
example.merge(links) <= you are using an observable created by the map callback in the map callback which would cause recursion (ie. loop back into itself). This is where it pays to use proper indention as it is easier to see.
ngOnInit(): void {
const source = this.categoryService.getCategories();
console.log(source instanceof Observable);
const example = source.map((categor) => categor.map((categories) => {
const links = this.categoryService.countCategoryLinks(categories.id);
// this line is the issue. You are using variable example which is being created by the callback this line of code is in
const aff = example.merge(links).subscribe(linke => console.log(linke));
return categories.id
}));
}
I am thinking maybe you did not mean to still be inside of map at this point?