There is a base that contains text from inputs:
let data={
textInput:"",
inputInfo:{size:""},
};
export const loginReducer=(state=data,action)=>{
switch (action.type) {
case "ChangeInputInfoSize":
if (Number(action.size)|| action.size==="" ){
let Statecopy={...state};
Statecopy.inputInfo.size=action.size;
return {...Statecopy};
}else {
return state
}
default:
return state;
}
}
export const ChangeInputInfoSizeAC=(size)=>({
type:"ChangeInputInfoSize",
size
});
As well as the component and its container:
import React from "react"
import {connect} from "react-redux";
import {DataFilling} from "./DataFilling";
import {ChangeInputInfoSizeAC} from "../store/loginReducer";
let MapStateToProps=(state)=>{
return {
inputInfo:state.loginReducer.inputInfo,
textInput:state.loginReducer.textInput
}
};
export const DataFillingContainer=connect(MapStateToProps,{ChangeInputInfoSizeAC})(DataFilling)
Component:
import React from "react"
export let DataFilling = (props) => {
let ChangeInputInfoSizeFunc = (e) => {
props.ChangeInputInfoSizeAC(e.target.value)
};
<input placeholder="size" value={props.inputInfo.size} onChange={ChangeInputInfoSizeFunc} />
}
When you try to fill the field, there is no change in the field, but if you replace inputInfo.size with textInput everywhere, then everything will work.
What needs to be changed in the first option for it to work?
I guess problem is in reducer where creating Statecopy
let Statecopy={...state};
Statecopy.inputInfo.size=action.size;
In first line let Statecopy={...state} will create a new object {textInput, inputInfo} but in second line Statecopy.inputInfo actually referencing to the old inputInfo, so it overwrite size in old inputInfo and keep its reference unchanged.
I recommend make states flat as far as possible or make sure creating new state.
let Statecopy={...state,inputInfo:{size:action.size}};
In addition, you can do few improvement to your code by following these steps:
Camel case naming is Javascript convention, so instead of let Statecopy write let stateCopy
Dispatch type convention is uppercase separated with underlines, so instead of "ChangeInputInfoSize" write "CHANGE_INPUT_INFO_SIZE"
When declaring a function use const because you don't want accidentally overwrite a function, so const ChangeInputInfoSizeFunc = (e) => {... is correct. This is true for export const DataFilling as well.
For a controlled component like DataFilling only dispatch the final state, so onChange event will save value in a local state, then an onBlur event dispatch the result to reducer.
Limit declaring new variables as far as possible, so in reducer you don't need stateCopy,
Make reducer initial state a const variable, you don't want overwrite it.
in if (Number(action.size)|| action.size==="" ) first part will be a number but second part will be an empty string. Because of consistency and preventing future bugs make both same. Also you can do this validation before dispatching size to the reducer.
Hopefully, all these recommendations will help to avoid future bugs
I had to do deep copy
case "ChangeInputInfoSize":{
if (Number(action.size)|| action.size==="" ){
let copyState = {...state};
copyState.inputInfo = {...state.inputInfo};
copyState.inputInfo.size=action.size
return {...copyState};
}else {
return state
}
}
Related
I'm having a question to immer.js with reactjs and redux.
I'm familiar with reactjs.
As far as I know it is not "allowed" to update a property itself by
props.someValue= 'someValue'
So you need to pass a callback function to the component itself like
<SomeComponent
key={"componentKey"}
someValue={"someValue"}
onSomeValueChange={this.handleSomeValueChange.bind(this)}
/>
and in SomeComponent you call the function like this:
...
this.props.onSomeValueChange('someNewValue');
...
OR
you can handle this with redux.
import React, { useState, useEffect } from 'react';
import { connect } from 'react-redux';
import { updateSomeValue } from '../actions/index';
function SomeComponent(props) {
function onTextChanged(event) {
props.updateSomeValue(event.target.value);
}
return (
<>
<input type="text" value={someValue} onChange={onTextChanged} />
</>
)
}
function mapStateToProps({ someValue }) {
return {
someValue
};
}
function mapDispatchToProps(dispatch) {
return {
updateSomeValue: someValue => dispatch(updateSomeValue(someValue)),
};
}
const Form = connect(
mapStateToProps,
mapDispatchToProps
)(SomeComponent);
export default Form;
The code above is a very simple example, because someValue is just a string and no complex object.
Now, if it is getting more complex and you have objects with subobjects, I'm still searching for the best way to update the subobjects.
In the past, I've used lodash to create a "clone" of the object and modified the "clone", before I updated the original property.
function onTextChanged(event) {
let updatedJob = _.cloneDeep(props.currentJob);
for(let a = 0; a < updatedJob.job_texts.length; a++) {
if(updatedJob.job_texts[a].id === props.jobText.id) {
updatedJob.job_texts[a].text = event.target.value;
}
}
props.updateCurrentJob(updatedJob);
}
This solution did work, but as you can see, it is probably not best way to handle this.
Now I read today, that my solution is not recommended as well. You need to create a "copy" of each subobject as far as I understood.
Then I stumbled across redux page about immer.js, but I'm not quite sure, how to use this:
The situation is as follows:
I have an object currentJob, which have several properties. One of these properties is a subobject (array) called job_texts.
One job_text has a property text which I need to update.
I thought, I can handle this, this way:
let updatedJob = props.currentJob;
props.updateCurrentJob(
produce(updatedJob.job_texts, draftState => {
if(draftState.id === props.jobText.id) {
draftState.text = text;
}
})
);
...but of course this won't work, because with the code above I'm updating currentJob with the subobject array.
Can anyone please give me a hint, how I use immer.js to update one job_text within the object currentJob?
Many thanks in advance!
The best way would be to use the official Redux Toolkit, which already includes immer in reducers created with createReducer and createSlice.
It's also generally the recommended way of using Redux since two years.
Generally, you should also have that "subobject update logic" in your reducer, not your component.
So with RTK, a "slice" would for example look like
const objectSlice = createSlice({
name: 'object',
reducers: {
updateSubObjectText(state, action) {
const jobText = state.job_texts.find(text => text.id === action.payload.id)
if (jobText) {
jobText.text = action.payload.text
}
}
}
})
export const { updateSubObjectText } = objectSlice.actions
export default objectSlice.reducer
This will create a reducer and an action creator updateSubObjectText to be used in your component. They will be hooked together and action types are an implementation detail you do not care about.
In your component you would now simply do
dispatch(updateSubObjectText({ id, text }))
For more infos see this quick into to Redux Toolkit or the full official Redux "Essentials" tutorial which nowadays covers Redux Toolkit as the recommended way of writing real Redux logic.
There is also a full docs page on writing Reducers with immer.
i'm creating a react component: TxtEditor
Inside the editor, there is a useReducer hook for manipulating the text:
APPEND => to alter the current text.
UPPERCASE => to convert uppercase letter.
But the reducer function is not a pure function. There are some dependencies:
props.disabled => ignores all the reducer actions.
props.onTxtChanged => will be executed after the text has modified.
So I created the reducer function inside the function component.
This is a problem for useReducer because each time the component rendered, a new identical function with different by reference always re-created.
Thus making useReducer executing the reducer function twice on next render -and- triggering props.onTxtChanged twice too. I don't know why react doing this.
Then to solve the problem, I wrapped the reducer function with useCallback.
It seem be working, but NOT. Because the props.onTxtChanged might be passed by user with an inline function. And the function always be re-created (identical but different by reference), thus making useCallback useless.
And finally I created a reducer function outside the function component.
The function is always the same by reference and making useReducer working properly.
To inject the dependencies I made a HACK like this:
const [state, dispatch] = useReducer(txtReducer, /*initialState: */{
text : 'hello',
props : props, // HACK: a dependency is injected here
});
state.props = props; // HACK: a dependency is updated here
So the props can be accessed in the reducer function:
const txtReducer = (state, action) => {
const props = state.props;
if (props.disabled) return state; // disabled => no change
}
It's working but it contain a hack.
I want the professional way doing this stuff.
Do you have any suggestion?
See the complete sandbox code here
This is what I would do:
function TxtEditor(props) {
const [state, dispatch] = useReducer(txtReducer, { text : 'hello'});
// Wait for the state to change and only then emmit a text change
useEffect(() => {
props.onTextChange(state.text);
}, [state])
return <input onInput={handleInput} />
function handleInput() {
if (props.disabled) return; // <-- just don't fire an update
// [...] call to your reducer
}
}
Reducers are just a useState with a bit of logic. So only let them handle a (singular) state and don't make it responsible for many things at once. Also they should only be responsible for the actual state logic, not something outside, like if the text box is disabled or not.
A potential solution to the desired hook described in the comments of this post:
I hope you don't mind the typescript. I just find it easier to work with.
enum TextProcessorMode {
APPEND,
}
interface TextProcessorActionOptions {
disabled?: boolean;
mode: TextProcessorMode;
}
interface TextProcesserAction {
newText: string;
options: TextProcessorActionOptions;
/**
* Is called when the text was successfully processed.
*/
onChange(text: string): void;
}
export default function textProcessorReducer(
state: string,
action: TextProcesserAction
): string {
if (action.options.disabled) return state;
let newState: string;
switch (action.options.mode) {
case TextProcessorMode.APPEND:
newState = state + action.newText;
break;
// Handle other modes
}
action.onChange(newState);
return newState;
}
I have a redux (sub) state that consists of a large number of similar entries.
export type PartnerCalculatorStateShape = {
m16_19:number;
m20_24:number;
m25_34:number;
m35_44:number;
m45_64:number;
m65_plus:number;
f16_19:number;
f20_24:number;
f25_34:number;
f35_44:number;
f45_64:number;
f65_plus:number;
};
I am using the Redux Toolkit so my reducer is of this form (note that Redux Toolkit uses immutable update logic, so I can assign modify the state directly)
type PartnerCalculatorPayload = {
key:string;
value:number;
}
export const partnerCalculatorSlice = createSlice({
name: 'PartnerCalculator',
initialState,
reducers: {
partnerCalculatorValueReceived(state, action: PayloadAction<PartnerCalculatorPayload>) {
state[action.payload.key] = action.payload.value;
}
}
});
I'm a bit stuck on how to use useSelector. What I want to do is have a selector function in my Redux file, something like this
export const selectorFunction = (state,key) => state[key]
where key would be m20_24, for example. Then I would use that selector function in my React component
const myVar = useSelector(selectorFunction)
But how do I pass in the key?
The official hooks documentation recommends using closure to pass additional variables
import React from 'react'
import { useSelector } from 'react-redux'
export const TodoListItem = props => {
const todo = useSelector(state => state.todos[props.id])
return <div>{todo.text}</div>
}
However. useSelector will be in my React component and I want to keep the selector function I pass to useSelector inside my redux file, so I can't see how to use closure.
I suppose I could just pass the entire state out from my selector function
const selectorFunction = state => state
and then treat it as an object in my React component and key into it there
const myState = useSelector(selectorFunction)
const myVar = myState["m20_24"]
but that seems kind of ugly.
If that's the way to go, would myVar update anytime any of the fields in my Redux state changed? I'm a bit unclear as to how the useSelector equality testing mechanism works -- it says it uses 'strict equality', so if any part of my state object changed (that is, if the field 'm20_24' changed) then myVar would be updated?
Thanks for any insights!
Pass an anonymous selector to useSelector, and then call the actual selector inside of there:
const value = useSelector(state => selectValueByKey(state, props.key));
I am trying to find a good, clean, with little boilerplate, way to handle React's global state
The idea here is to have a HOC, taking advantage of React's new Hooks & Context APIs, that returns a Context provider with the value bound to its state. I use rxjs for triggering a state update on store change.
I also export a few more objects from my store (notably : the raw rxjs subject object and a Proxy of the store that always returns the latest value).
This works. When I change something in my global store, I get updates anywhere in the app (be it a React component, or outside React). However, to achieve this, the HOC component re-renders.
Is this a no-op ?
The piece of code / logic I think could be problematic is the HOC component:
const Provider = ({ children }) => {
const [store, setStore] = useState(GlobalStore.value)
useEffect(() => {
GlobalStore.subscribe(setStore)
}, [])
return <Context.Provider value={store}>{children}</Context.Provider>
}
GlobalStore is a rxjs BehaviorSubject. Every time the subject is updated, the state of the Provider component gets updated which triggers a re-render.
Full demo is available there: https://codesandbox.io/s/qzkqrm698q
The real question is: isn't that a poor way of doing global state management ? I feel it might be because I basically re-render everything on state update...
EDIT: I think I have written a more performant version that's not as lightweight (depends on MobX), but I think it generates a lot less overhead (demo at: https://codesandbox.io/s/7oxko37rq) - Now what would be cool would be to have the same end result, but dropping MobX - The question makes no sense anymore
I understand your need to handle a global state. I already found myself in the same situation. We have adopted similar solutions, but in my case, I've decided to completelly drop from ContextAPI.
The ContextAPI really sucks to me. It seems to pretend to be a controller based pattern, but you end up wrapping the code inside an non-sense HOC. Maybe I've missed he point here, but in my opinion the ContextAPI is just a complicated way to offer scoped based data flow.
So, I decided to implement my own global state manager, using React Hooks and RxJS. Mainly because I do not use to work on really huge projects (where Redux would fit perfectly).
My solution is very simple. So lets read some codes because they say more than words:
1. Store
I've created an class only to dar nome aos bois (it's a popular brazilian expression, google it 😊) and to have a easy way to use partial update on BehaviorSubject value:
import { BehaviorSubject } from "rxjs";
export default class Store<T extends Object> extends BehaviorSubject<T> {
update(value: Partial<T>) {
this.next({ ...this.value, ...value });
}
}
2. createSharedStore
An function to instantiate the Store class (yes it is just because I don't like to type new ¯\(ツ)/¯):
import Store from "./store";
export default function <T>(initialValue: T) {
return new Store<T>(initialValue);
}
3. useSharedStore
I created an hook to easily use an local state connected with the Store:
import Store from "./store";
import { useCallback, useEffect, useState } from "react";
import { skip } from "rxjs/operators";
import createSharedStore from "./createSharedStore";
const globalStore = createSharedStore<any>({});
type SetPartialSharedStateAction<S> = (state: S) => S;
type SetSharedStateAction<S> = (
state: S | SetPartialSharedStateAction<S>
) => void;
export default function <T>(
store: Store<T> = globalStore
): [T, SetSharedStateAction<T>] {
const [state, setState] = useState(store.value);
useEffect(() => {
const subscription = store
.pipe(skip(1))
.subscribe((data) => setState(data));
return () => subscription.unsubscribe();
});
const setStateProxy = useCallback(
(state: T | SetPartialSharedStateAction<T>) => {
if (typeof state === "function") {
const partialUpdate: any = state;
store.next(partialUpdate(store.value));
} else {
store.next(state);
}
},
[store]
);
return [state, setStateProxy];
}
4. ExampleStore
Then I export individual stores for each feature that needs shared state:
import { createSharedStore } from "hooks/SharedState";
export default createSharedStore<Models.Example | undefined>(undefined);
5. ExampleComponent
Finally, this is how to use in the component (just like a regular React state):
import React from "react";
import { useSharedState } from "hooks/SharedState";
import ExampleStore from "stores/ExampleStore";
export default function () {
// ...
const [state, setState] = useSharedState(ExampleStore);
// ...
function handleChanges(event) {
setState(event.currentTarget.value);
}
return (
<>
<h1>{state.foo}</h1>
<input onChange={handleChange} />
</>
);
}
GlobalStore subject is redundant. RxJS observables and React context API both implement pub-sub pattern, there are no benefits in using them together this way. If GlobalStore.subscribe is supposed to be used in children to update the state, this will result in unnecessary tight coupling.
Updating glubal state with new object will result in re-rendering the entire component hierarchy. A common way to avoid performance issues in children is to pick necessary state parts and make them pure components to prevent unnecessary updates:
<Context.Consumer>
({ foo: { bar }, setState }) => <PureFoo bar={bar} setState={setState}/>
</Context.Provider>
PureFoo won't be re-rendered on state updates as long as bar and setState are the same.
I'm building an app that contains a store with an "offers" section of the state tree (ImmutableJS List Object). I need to take some action (play a browser sound) whenever an item is added to this list. Items can be added to this list via several different types of Redux actions.
I am trying to figure out the best way to react to the changes to a particular part of the store. I could do it in each action/reducer method, but then I would have it all over the place. I'd rather have one central place to handle the logic.
What's the best way to handle this? Should I create a generic store subscriber and has it's own logic for keeping track of the list values?
In this case your best bet is a store listener. Either a plain listener function or a redux connected React component.
Assuming a simple function to make noise:
function playSound () {
const audio = new Audio('audio_file.mp3')
audio.play()
}
You can create a store observer and listen for changes:
function createSoundObserver (store) {
let prevState = store.getState()
return store.subscribe(() => {
const nextState = store.getState()
if (prevState.messages.length < nextState.messages.length) {
playSound()
}
prevState = nextState
})
}
You can achieve the same with a React component:
import React, {Component, PropTypes} from 'react'
import {connect} from 'react-redux'
class Notifier extends Component {
static propTypes = {
messages: PropTypes.array.isRequired
}
componentDidUpdate (prevProps) {
if (this.props.messages.length > prevProps.messages.length) {
playSound()
}
}
render () { return null }
}
export default connect((state, props) => {
const {messages} = state
return {messages}
}, {})(Notifier)
As long as a Notifier is present amongst the rendered tree, it will check for changes and play the sound accordingly. The advantage of this approach is that you don't have to take extra care of unsubscribing the event if you want to stay quiet, and it seamlessly works server-side rendering.