React Custom Hook not triggering DOM updates - javascript

Here's a sandbox reproducing my issue:
https://codesandbox.io/s/ymmyr3o70x?expanddevtools=1&fontsize=14&hidenavigation=1
For reasons I can't understand, when I add product via the form, the product list is only updated once, then never again.
I'm using a custom hook (useObservable) combined with RxJS to manage state. If you check the console logs, the ReplaySubject does emit the expected values. But the useObservable hook is not triggering an update of the DOM.
What am I missing?

The issue is that your addProduct function mutates the old state instead of creating a new one. Yes, you have the observable emit the state again, but since its the same object as before, calling setValue has no effect and so react does not rerender.
The solution to this is to make the state immutable. For example:
import { ReplaySubject } from "rxjs";
let products = {};
export const products$ = new ReplaySubject(1);
export const addProduct = product => {
products = {...products, [product]: product};
products$.next(products);
};

Related

Custom react hook not updating it's value

So I have the following custom hook, I want to use it to enable/disable some buttons based on various triggers inside the app.
import { useState } from 'react';
interface IDisableProps {
buttonsDisabled: boolean;
toggleButtons: (isDisabled: boolean) => void;
}
export default function useDisable(): IDisableProps {
const [buttonsDisabled, setButtonsDisabled] = useState<boolean>(false);
const toggleButtons = (isDisabled: boolean) => {
setButtonsDisabled(isDisabled);
};
return {
buttonsDisabled,
toggleButtons,
};
}
One of the places I'm using it from is another hook, where I declare it as
const { buttonsDisabled, toggleButtons } = useDisable(); then use it at the right moment like
if (!buttonsDisabled) {
toggleButtons(true);
}
However, the state always remains the initial one. Upon entering with debugger in toggleButtons I see that in the local scope, this is undefined and can't see the value of buttonsDisabled. What am I missing here? Did I take the wrong approach?
From Building Your Own Hooks – React
Do two components using the same Hook share state? No. Custom Hooks are a mechanism to reuse stateful logic (such as setting up a subscription and remembering the current value), but every time you use a custom Hook, all state and effects inside of it are fully isolated.
So when you say "one of the places [you're] using it from is another hook" you are creating an additional [buttonsDisabled, setButtonsDisabled] in memory and not referencing the same state created in the other place(s) you've invoked useDisable.
Two ways to share state are 1) passing props around and 2) using Context.
try with
const toggleButtons = useMemo(isDisabled: boolean) => {
setButtonsDisabled(isDisabled)
}, [isDisabled]);

Fetch graphql query result on render and re-renders BUT LAZILY

I have a React Apollo app and what I am trying to do is that I have a component that renders some data using charts. For this data, I have some filters that I save in the local state of the component (Using hooks)
const [filters, setFilters] = useState(defaultFilters);
Now what I want is that whenever the component mounts, fetch the data using the default filters. But I also want to re-fetch data when the user updates the filters AND CLICKS ON SUBMIT and I'd fetch the results using new filters.
Since I also want to fetch the results on filter update, I am using useLazyQuery hook provided by apollo
const [getData, {data}] = useLazyQuery(GET_DATA_QUERY, { variables: {filters} });
useEffect(getData, []); // this useEffect runs only when the component mounts and never again
But, what happens is whenever my state, filters, updates the getData function is automatically run! ALWAYS! (BEHIND THE SCENE)
How do I handle such cases, where I want to fetch results on mounting and re-rendering.
I have tried using useQuery and refetch provided by it but I get the same problem there, whenever I update the state, the component rerenders and the useQuery hooks is run and makes the call. (That's how I believe it runs)
How do I fix my current code. Calling the getData function inside the useEffect function makes it run on every re-render.
I think I the problem defined in this stackoverflow-question is somewhat similar to mine.
Part of the problem is that you really have two different states that you're trying to utilize a single hook for. You have state that represents your inputs' values in the UI, and then you have state that represents the filters you want to actually apply to your charts. These are two separate bits of state.
The simplest solution is to just do something like this:
const [inputFilters, setInputFilters] = useState(defaultFilters)
const [appliedFilters, setAppliedFilters] = useState(inputFilters)
const { data } = useQuery(GET_DATA_QUERY, { variables: { filters: appliedFilters } })
const handleSubmit = () => setAppliedFilters(inputFilters)
const handleSomeInputChange = event => setInputFilters(...)
This way, you use inputFilters/setInputFilters only to manage your inputs' state. When the user clicks your submit button, the appliedFilters are set to whatever the inputFilters are at the time, and your query will update to reflect the new variables.

Calling state changing functions from mapStateToProps

I am new to react-redux and I was surprised to see an example where a function, in this case being getVisiblieTodos, is called inside mapStateToProps. This function should be called in a reducer since it changes state? Is the code breaking "good form" for the sake of brevity? Is it okay to do this in general?
I am looking at code from this link
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
In redux we want the store to hold the minimal data needed for the app. Everything that is derived from the base data, should be computed on the fly, to prevent cloning pieces of the store, and the need to recompute all derived data when something changes in the store.
Since the visible todos list is not part of the store, but computed using the list of todos, and the visibilityFilter, the getVisibleTodos() doesn't change the store's state. It produces the derived computed data from the those two properties.
A function that is used to get data from the store, and compute derived data is known as a selector. Using selectors, the derived data is not part of the store, and computed when needed. In addition, we can use memoized selectors, to save the computation overhead.
You may see getVisibleTodos as a reducer because it includes "switch .. case" block or/and because it has 2 arguments . However, it is not a rule.
A redux reducer ( by definition) changes store state according to dispatched action , and that's why it takes two arguments ( store state + dispatched action ) and it returns new state for the store without mutation.
getVisibleTodos here is a helper function which filter an array according to string (filter).
Also , filter is not a redux-action, it is just string that decides todos to be rendered.
I may agree with you it is something weird , and if we can see the whole application (reducers, actions,... ) we can decide if it is best practices or not .
todos in this component is a calculated property based on the state of the reducer, and it is not changing any state.
It's okay to transform properties comming from recuders that are used only by one component (they are called selectors). Imagine that you use todos in other components, you will not want to make changes in one component like filtering and seeing that in the other components. If this is the case, it's fine to do it.
Also, it is a good property of your reducer to store only the needed data. More state is more complexity in the app, and more overhead to calculate new states.
It seems to me that a function should do what its name says, nothing less, nothing more.
mapStateToProps() should just do that, ie "map", and should normally not call other functions.

Angular 2 - ngrx subscribe running on every state change

I have a basic redux implementation with ngrx/store.
// root reducer
export const root = {
posts: fromPosts.reducer,
purchases: fromPurchases.reducer
}
export function getAllPosts( state ) {
return fromPosts.getAll(state.posts);
}
export function getAllPurchases( state ) {
return fromPurchases.getAll(state.purchases);
}
In my component, I'm selecting the state pieces.
this.posts$ = store.select(getAllPosts).do(c => console.log('posts$ change'));
this.purchases$ = store.select(getAllPurchases).do(c => console.log('purchases$ change'));
But on every change to the state, both the handler is running. For example, when I am adding a post, the purchases$ handler also runs.
All point is to run only the part that changed, or I am wrong?
This answer is wrong, but, for some reason, it was accepted. The implementation of select uses distinctUntilChanged, so the problem likely resides in the implementations of the OP's getAll functions.
Whenever an action is dispatched to the store, it's passed to the composed reducers and a new state is composed. That new state is then emitted.
So both of your observables will see a value emitted to the do operators, even when the selected slice of state has not changed.
However, it's simple to change this behaviour by using the distinctUntilChanged operator:
import 'rxjs/add/operator/distinctUntilChanged';
this.posts$ = store.select(getAllPosts)
.distinctUntilChanged()
.do(c => console.log('posts$ change'));
distinctUntilChanged will ensure that the observable only emits when the value has actually changed, so if the slice of state you are selecting has not changed, nothing will be emitted.
Your selectors are not using createSelector.
When using createSelector & when state changes createSelector memoizes return value based on it's parameter values.
When function defined with createSelector gets called again with the same parameters memoized return value is given & rest of the selector code is not run.
/**
* Every reducer module exports selector functions, however child reducers
* have no knowledge of the overall state tree. To make them usable, we
* need to make new selectors that wrap them.
*
* The createSelector function creates very efficient selectors that are memoized and
* only recompute when arguments change. The created selectors can also be composed
* together to select different pieces of state.
*/
export const getBookEntitiesState = createSelector(
getBooksState,
state => state.books
);
Example use (albeit this is an old thread and example is from a newer NGRX)
https://github.com/ngrx/platform/blob/master/example-app/app/books/reducers/index.ts#L58
Since The implementation of select uses distinctUntilChanged ,
and
distinctUntilChanged uses === comparison by default, object references
must match.
https://www.learnrxjs.io/operators/filtering/distinctuntilchanged.html
probably the problem relies in the fact that no matter what action is dispatched to your reducer , you return a new reference of the state (even when the state value has not been changed).
check in your reducer switch statement,the default section, probably it deep copies the state when no values are changed - therefore returnning a new reference and the selector executes again.

Listening for ImmutableJS changes?

I want to trigger a function whenever a new ImmutableJS object is created. I want to use an ImmutableJS object as my React application state and I want to reduce boilerplate code.
Here's part of what I have now:
function addTodo(text) {
let state = store.getState(); // Gets ImmutableJS object.
let todos = state.get('todos');
let newState = state.set('todos', todos.push('text'));
store.update(newState); // Triggers rerender.
}
Here's what I want it to be like:
function addTodo(text) {
let state = store.getState(); // Gets ImmutableJS object.
state.get('todos').push(text); // Store listens to change and replaces the current state with the newly created state. Store automatically triggers rerender.
}
Is this possible?
I tried wrapping the entire addTodo in withMutations, but withMutations allows only shallow mutations.
Agree with Lukas' comment. This is really what Redux is for. You mutate the state of your stores and it will force a re-render of your component tree(s) with the new values.
I'm not aware of any sort of observable functions in Immutable.js itself, there might be a third part library that has that functionality though.

Categories