How do I update state through a React context? - reactjs

Here's a trivial example using a React context:
type MyState = { counter: number };
const MyContext = createContext<[MyState, () => void]|undefined>(undefined);
class MyComponent extends Component<any, MyState> {
constructor(props: any) {
super(props);
this.state = { counter: 42 };
}
render() {
return (
<MyContext.Provider value={[this.state, () => this.setState({ counter: this.state.counter + 1 })]}>
{this.props.children}
</MyContext.Provider>
)
}
}
function App() {
return (
<MyComponent>
<MyContext.Consumer>
{
pair => {
const [state, increment] = pair || [];
return (<button onClick={increment}>Click to increment: {state?.counter}</button>);
}
}
</MyContext.Consumer>
</MyComponent>
);
}
This works, and clicking the button increments the counter as expected. But when I try to pull up the incrementer into a method on the parent component:
type MyState = { counter: number };
const MyContext = createContext<MyComponent|undefined>(undefined);
class MyComponent extends Component<any, MyState> {
constructor(props: any) {
super(props);
this.state = { counter: 42 };
}
render() {
return (
<MyContext.Provider value={this}>
{this.props.children}
</MyContext.Provider>
)
}
increment() {
this.setState({ counter: this.state.counter + 1 });
}
}
function App() {
return (
<MyComponent>
<MyContext.Consumer>
{
me => (<button onClick={() => me?.increment()}>Click to increment: {me?.state.counter}</button>)
}
</MyContext.Consumer>
</MyComponent>
);
}
Now the button no longer repaints when clicked.
The counter is incrementing, but the state change doesn't propagate to the consumer. Why is this?
I suppose it's possible to use a reducer instead of an object:
type MyState = { counter: number };
type MyAction = { type: "increment" };
const MyContext = createContext<[MyState, React.Dispatch<MyAction>]|undefined>(undefined);
function myReducer(state: MyState, action: MyAction) {
switch (action.type) {
case "increment":
return { counter: state.counter + 1 };
default:
throw new Error();
}
}
function MyComponent({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(myReducer, { counter: 42 });
return (
<MyContext.Provider value={[state, dispatch]}>
{children}
</MyContext.Provider>
)
}
function App() {
return (
<MyComponent>
<MyContext.Consumer>
{
pair => {
const [state, dispatch] = pair || [];
return (<button onClick={() => dispatch && dispatch({ type: "increment" })}>Click to increment: {state?.counter}</button>);
}
}
</MyContext.Consumer>
</MyComponent>
);
}
But this means cutting up existing code that's nicely-encapsulated in a class into a reducer function with a big switch statement. Is there a way to avoid this?

I think you should not put this as the value of the context on your provider.
When you're passing () => this.setState({ counter: this.state.counter + 1 })]} you're passing an anonymous function (() => {}) that runs your this.setState(). As it is an arrow function, it guarantees that this.setState and this.state are refereeing to the correct this.
If you'd like to use the increment method, you can keep using the same logic:
<MyContext.Provider value={[this.state, () => this.increment()]}>
Of course, you don't need to use an array if you want, you can use an object:
<MyContext.Provider value={{ state: this.state, increment: () => this.increment() }}>
This is common in React, and it's better to store in the context only the values you need to pass to the consumers, this way you can better understand which part of your code is changing what, and only have access to what it needs.

Related

Sorting Data in Reducer Actions

I am trying to create a sort button which when clicked will sort me menu cards alphabetically. My question is how should I have the sort function coded in the Reducer and Actions? I added pseudo-code for sorting in the Reducer as well. When I click the button I am getting "(TypeError): state.slice is not a function".
Edit:
Added my button component and main Container.
Actions:
export const sortMenus = () => {
return dispatch => {
dispatch({ type: "LOADING_MENUS" });
fetch(`/api/menus`)
.then(res => res.json())
.then(responseJSON => {
dispatch({ type: "SORT_MENUS", cards: responseJSON });
});
};
};
Reducer:
export default function MenusReducer(
state = {
cards: [],
loading: false
},
action
) {
switch (action.type) {
case "LOADING_MENUS":
return {
...state
};
case "ADD_MENUS":
return {
...state,
cards: action.cards
};
case "SORT_MENUS":
return state.slice().sort(function(menu1, menu2) {
if (menu1.name < menu2.name) return -1;
if (menu1.name < menu2.name) return 1;
return 0;
});
default:
return state;
}
}
Button Component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { sortMenus } from ".././actions/dataActions";
import Row from "react-bootstrap/Row";
import Container from "react-bootstrap/Container";
class SortButton extends Component {
constructor() {
super();
this.state = { menus: [] };
}
handleMenuSort = e => {
this.props.sortMenus()
};
render() {
return (
<Container>
<Row>
<div>
<button id="sort-button" title="Sort Menus" onClick= {this.handleMenuSort}>Sort Menus</button>
</div>
</Row>
</Container>
)
}
}
const mapStateToProps = state => {
return {
menus: state.menus
}
};
const mapDispatchToProps = dispatch => {
return {
sortMenus: params => dispatch(sortMenus(params)),
}
};
export default connect(mapStateToProps, mapDispatchToProps)(SortButton)
Container:
class MainContainer extends Component {
displayCards = () => {
switch(this.props.path) {
case "menus":
return (this.props.menus.cards.map(card => (
<NavLink style={{ color: "black" }} to={`/menus/${card.id}`} key={card.id}><MenuCard view={this.props.displayObject} info={card} /></NavLink>
)));
default:
return (<div>Empty</div>)
}
};
render() {
return (
<CardColumns>
{this.displayCards()}
</CardColumns>
)
}
}
const mapStateToProps = state => {
return {
menus: state.menus
}
};
const mapDispatchToProps = dispatch => {
return {
displayObject: (id, category, type) => dispatch(displayObject(id, category, type)),
}
};
export default connect(mapStateToProps, mapDispatchToProps)(MainContainer)
Your state is an object, not an array. You likely mean to sort the stored cards array.
state.cards.slice(... instead of state.slice(...
case "SORT_MENUS":
return state.cards.slice().sort(function(menu1, menu2) {
if (menu1.name < menu2.name) return -1;
if (menu1.name < menu2.name) return 1;
return 0;
});
Side note: You may also want to clear/set your loading state upon successful data fetching. ;)
EDIT
You are mapping undefined state within mapStateToProps, then mapping over it in the component. Change mapStateToProps to access the correct defined property.
const mapStateToProps = state => ({
cards: state.cards,
});
Then you can iterate over the new cards prop.
case "menus":
return (this.props.cards.map(card => (
<NavLink
style={{ color: "black" }}
to={`/menus/${card.id}`}
key={card.id}
>
<MenuCard view={this.props.displayObject} info={card} />
</NavLink>
)));
You can simply store the fetched menu in application state.
You can have standalone action say SORT_MENU_BY_ALPHABET.
You can simply dispatch this action on button handler as well as on Ajax success. this dispatch may not have any payload associated.
hope it helps.
in reducer you defined state as object and you're trying to do array operation on it. state.slice().
slice is a function available for arrays. so its throwing error.
you should be doing
state.cards.slice().sort((a,b)=> a-b)

How to make a custom hook with useState which can update multiple elements if state changes?

Consider the following example:
const useCounter = () => {
const [count, setCount] = useState(0);
return [ count, setCount ];
};
const Shower = () => {
const [ value ] = useCounter();
console.log(value); //stays 0
return value;
}
const Setter = () => {
const [ value, setValue ] = useCounter();
console.log(value); //updates on click
return <button onClick={() => setValue(value+1)}>
Add
</button>
}
const App = () => {
return (
<div className="App">
<Setter />
<Shower />
</div>
);
}
What am I doing wrong? I'd expect that it will use the same state no matter where and how many times it gets used, and if that state updates, it should update every component which uses it I think.
Any suggestions?
That's what react context api try to solve.
const CounterContext = React.createContext({
count: 0,
setCount: () => null
})
const CounterProvider = ({ children }) => {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={{
count, setCount
}}>
{children}
</CounterContext.Provider>
)
}
const useCounter = () => {
return React.useContext(CounterContext)
};
useCounter will now provide you the same count and setCount in every component you call it.
To use it:
const Shower = () => {
const { count } = useCounter();
return count;
}
const Setter = () => {
const { count, setCount } = useCounter();
return <button onClick={() => setCount(count+1)}>
Add
</button>
}
const App = () => {
return (
<CounterProvider>
<div className="App">
<Setter />
<Shower />
</div>
</CounterProvider>
);
}
useState returns a pair of value and setter. A piece of data and a way to change it, but everytime you instantiate a new Component a new instace of this pair will be created as well. hooks are a great way to share statetul logic between components, not state itself. Shower get's called and a instance of useCounter is created. Setter gets called and a new instance is created. The structure is the same, the state is not.
To share state between components use props, redux or Context API
When sharing things between functional components, I like to use the pattern below, it is the redux-ish reusable version of Federkun's answer above:
// this component should be an ancestor of component sharing state
// note that it works no matter how deep your subcomponents are in the render tree
class SharedStateContextProvider extends React.Component {
/* static propTypes = {
sharedContext: PropTypes.object,
reducer: PropTypes.func,
initialState: PropTypes.object,
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.arrayOf(PropTypes.node),
]),
} */
constructor(props) {
super(props);
this.state = {
contextValue: { state: props.initialState, dispatch: this.handleDispatch },
};
}
handleDispatch = (action) => {
const { reducer } = this.props;
const { contextValue: { state: sharedState } } = this.state;
const newState = reducer(sharedState, action);
if (newState !== sharedState) {
this.setState(
() => ({
contextValue: { state: newState, dispatch: this.handleDispatch }
})
);
}
}
render() {
const { sharedContext: Context, children } = this.props;
const { contextValue } = this.state;
return (
<Context.Provider value={contextValue}>
{children}
</Context.Provider>
);
}
}
// the actual shared context
const CounterContext = React.createContext();
// add as much logic as you want here to change the state
// as you would do with redux
function counterReducer(state, action) {
switch(action.type) {
case 'setValue':
return {
...state,
value: action.data
};
default:
return state;
}
}
// counterContext is a prop so the dependency in injected
const Shower = ({ counterContext }) => {
// well known redux-ish interface
const { state, dispatch } = React.useContext(counterContext);
console.log(state.value);
return state.value;
}
// counterContext is a prop so the dependency in injected
const Setter = ({ counterContext }) => {
// well known redux-ish interface
const { state, dispatch } = React.useContext(counterContext);
console.log(state.value); //updates on click
return <button onClick={() => dispatch({ type: 'setValue', data: state.value+1 })}>
Add
</button>
}
// the actual shared state
const initialCounterState = { value: 0 };
const App = () => {
return (
<div className="App">
<SharedStateContextProvider
sharedContext={CounterContext}
reducer={counterReducer}
initialState={initialCounterState}
>
<Setter counterContext={CounterContext} />
<Shower counterContext={CounterContext} />
</SharedStateContextProvider>
</div>
);
}
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Condition on checkbox in React with Redux

This is surely very simple but I dont understand how it works. I try to bind checkbox with state and with state display different string. It is in React with Redux. The code below (bold font)
container:
class DropingList extends Component {
**conditionHandler() {
if(this.props.pet === 'cat'){
return "YEAH!!!"
}else {return null;}**
}
render() {
return (
<div>
<AddHimHer
click={this.props.onAddMan}
/>
{ this.props.pers.map(per =>(
<NewPerson
key={per.id}
click={() => this.props.onManDown(per.id)}
name={per.name}
age={per.age}
**animal={this.conditionHandler(this.props.pet)}**
/>
))
}
</div>
)
}
}
const mapStateToProps = state => {
return {
pers: state.persons
}
}
const mapDispatchToProps = dispatch => {
return {
onAddMan: (name,age,**pet**) => dispatch({type:actionTypes.ADD_MAN, data: {nam: name, ag: age, **superp: pet**}}),
onManDown: (id) => dispatch({type:actionTypes.MAN_DOWN, Id: id})
}
}
export default connect(mapStateToProps,mapDispatchToProps)(DropingList);
component:
const NewPerson = (props) => (
<div onClick={props.click}>
<h1>Is {props.name} a SUPERHERO? ? ???</h1>
<h2>He is {props.age} years old</h2>
**<h1>{props.animal}</h1>**
</div>
);
export default NewPerson;
reducer:
const initState = {
persons: []
}
const personReducer = (state = initState,action) => {
switch (action.type) {
case actionTypes.ADD_MAN:
const newMan = {
id: Math.random(),
name: action.data.nam,
age: action.data.ag,
**pet: action.data.superp**
};
return {
...state,
persons: state.persons.concat(newMan)
};
case actionTypes.MAN_DOWN:
return {
...state,
persons: state.persons.filter(person => person.id !== action.Id)
};
}
return state;
};
export default personReducer;
I am still newbe in React and Redux. I think I have ommited something.
Could you tell me whats wrong with my code?
Issue is pet is the part of the object (each object of the array), not a separate prop so you need to use per.pet in map callback function, like this:
{this.props.pers.map(per =>(
<NewPerson
key={per.id}
click={() => this.props.onManDown(per.id)}
name={per.name}
age={per.age}
animal={this.conditionHandler(per.pet)} // here
/>
))}
Now you are passing the pet value to function conditionHandler, so no need to use this.props.pet inside that directly use pet, like this:
conditionHandler(pet) {
if (pet === 'cat') {
return "YEAH!!!"
} else {
return null;
}
}

ReactJs redux : How to call a function from the render function if the prop value change?

I have two react components which are ProgramSearchBox and DualBox which are generic and wrapper components of predefined npm packages AutoSuggest and DualListBox respectively.
My task to achieve is Based on the value from ProgramSearchBox, I have to list the set values in the DualListBox.
So, If user select a Program from ProgramSearchBox, then I will call API by passing the ProgramId and fetch the set of result values and have to bind them in the DualListBox.
I will get the user selected ProgramID from the ProgramSearchBox as a prop in DualBox component render method.
How to dispatch an action (call a function) from render function in DualBox component by passing the ProgramId?
If I call a method from render function in DualBox, that is becoming Infinite loop!
Here is DualBox component:
//DualBox.js
class DualBox extends React.Component {
constructor() {
super();
this.state = { selected: [] };
this.onChange = this.onChange.bind(this);
this.options = [ ];
}
onChange(selected) {
selected(selected);
}
updateOptions()
{
console.log("Update Option method called :" + this.props.traineesList );
this.options = [{ value: 'luna', label: 'Moon' }, { value: 'phobos', label: 'Phobos' }];
//this.options = this.props.traineeList.map( (value,id) => )
}
render() {
const {ProgramID} = this.props; // HERE I GET ProgramID AS PROP FROM AN ANOTHER COMPONENT
const {selected} = this.state;
if(ProgramID !== "") // BASED ON THIS ProgramID VALUE, I NEED TO DISPATCH AN ACTION.
{
{this.updateProgramId(ProgramID)} // THIS IS CAUSING INFINITE LOOP
{this.updateOptions}
console.log("Program Id came to dualbox:" +ProgramID);
return <DualListBox options={this.options} selected={selected} onChange={this.onChange}
canFilter
filterCallback={(option, filterInput) => {
if (filterInput === '') {
return true;
}
return (new RegExp(filterInput, 'i')).test(option.label);
}}
filterPlaceholder="Filter..."
/>;
}
else
{
console.log("Program Id didn't come to dualbox");
return <DualListBox options={this.options} selected={selected} onChange={this.onChange}
canFilter
filterCallback={(option, filterInput) => {
if (filterInput === '') {
return true;
}
return (new RegExp(filterInput, 'i')).test(option.label);
}}
filterPlaceholder="Filter..."
/>;
}
}
}
function mapStateToProps(state, ownProps) {
return {
traineesList: state.traineesList
};
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
updateProgramId: bindActionCreators(( {ProgramID}) => dualBoxActions.getTraineesList(ProgramID), dispatch)
};
}
export default connect(mapStateToProps,mapDispatchToProps)(DualBox);
Here is the ProgramSearchBox component:
function renderSuggestion(suggestion) {
return (
<ul>
<li>{suggestion.Program}</li>
</ul>
);
}
class ProgramSearchBox extends React.Component {
constructor(props) {
super(props);
}
render() {
const { value, suggestions, onChange, onSuggestionSelected} = this.props;
const inputProps = {
placeholder: "Look Up",
value,
onChange: (event, { newValue, method }) => {
this.setState({
value: newValue
});
console.log("onChange: " + JSON.stringify(newValue) );
onChange(newValue);
}
};
return (
<Autosuggest
suggestions={suggestions}
onSuggestionsFetchRequested={this.props.onSuggestionsFetchRequested}
onSuggestionsClearRequested={this.props.onSuggestionsClearRequested}
onSuggestionSelected={
(event, { suggestion, suggestionValue, suggestionIndex, sectionIndex, method }) => {
console.log("onSuggestionSelected: " + JSON.stringify(suggestion) );
onSuggestionSelected(suggestion);
}
}
getSuggestionValue={(suggestion) => suggestion.Program}
renderSuggestion={renderSuggestion}
inputProps={inputProps}
theme={theme}
/>
);
}
}
function mapStateToProps(state, ownProps) {
return {
suggestions: state.results
};
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onSuggestionsFetchRequested: bindActionCreators(({ value }) => searchActions.getProgramSuggestions(value), dispatch),
onSuggestionsClearRequested: bindActionCreators(() => searchActions.clearSuggestions(), dispatch),
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ProgramSearchBox);
Don't call other functions in render() method. Render method is responsible only for rendering views, it can be called many times and it should be as pure as possible.
Updated answer (2019-11-21)
Use componentDidUpdate(prevProps) lifecycle function to react to prop changes.
It will look something like this:
componentDidUpdate(prevProps) {
if (this.props.ProgramID !== '' && prevProps.ProgramID !== this.props.ProgramID) {
this.updateProgramId(this.props.ProgramID)
}
}
Old answer
To do actions depending on props changing, use componentWillReceiveProps(nextProps) lifecycle function.
It will look something like this:
componentWillReceiveProps(nextProps) {
if (nextProps.ProgramID !== '' && this.props.ProgramID !== nextProps.ProgramID) {
this.updateProgramId(ProgramID)
}
}
After calling this.updateProgramId(ProgramID) props will update and render method will be called.
More info about ReactJS lifecycle:
https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops

how to increment and decrement state value in react?

I am trying to increment and decrement state value in react using react-redux.I add actions , container ,reducer .But I don't know how to subscribe the increment and decrement action here is my code
I want to increment and decrement the value when user click on buttons
here is my code
http://codepen.io/anon/pen/jVjMXv?editors=1010
const abc= (state=0,action) => {
console.log(action.type)
switch(action.type){
case 'INCREMENT':
return state +1
case 'DECREMENT':
return state -1
Default :
return state;
}
}
const {createStore,bindActionCreators} =Redux;
const {Provider,connect} =ReactRedux;
const store = createStore(abc);
class First extends React.Component {
constructor (){
super();
this.state ={
digit :0
}
}
inc (){
console.log('ince')
}
dec (){
console.log('dec')
}
render(){
return (
<div>
<button onClick={this.inc.bind(this)}>INCREMENT</button>
<p>{this.state.digit}</p>
<button onClick={this.dec.bind(this)}>DECREMENT</button>
</div>
)
}
}
const actions = {
increment: () => {
return {
type: 'INCREMENT',
}
},
decrement: () => {
return {
type: 'DECREMENT',
}
}
};
const AppContainer = connect(
function mapStateToProps(state) {
return {
digit: state
};
},
function mapDispatchToProps(dispatch) {
return bindActionCreators(actions, dispatch);
}
)(First);
ReactDOM.render(
<Provider store={store}>
<First/>
</Provider>
,document.getElementById('root'))
You need to make a lot of changes
First: Since you are connecting your First component to state and actions as AppContainer you need to render it in DOM
ReactDOM.render(
<Provider store={store}>
<AppContainer/>
</Provider>
,document.getElementById('root'))
Second: you are dispatching actions INC and DEC and you are handling INCREMENT, DECREMENT in reducer
Third: You should render the state you get from redux and not the component state like
{this.props.digit}
Fourth:
call the action via the props like this.props.increment(), this.props.decrement()
Complete Code
const abc= (state=0,action) => {
console.log('in redux', action.type)
switch(action.type){
case 'INC':
return state +1
case 'DEC':
return state -1
default :
return state;
}
}
const {createStore,bindActionCreators} =Redux;
const {Provider,connect} =ReactRedux;
const store = createStore(abc);
class First extends React.Component {
constructor (props){
super(props);
this.state ={
digit :0
}
}
inc (){
console.log('ince', this.props)
this.props.increment();
}
dec (){
console.log('dec')
this.props.decrement();
}
render(){
return (
<div>
<button onClick={this.inc.bind(this)}>INCREMENT</button>
<p>{this.props.digit}</p>
<button onClick={this.dec.bind(this)}>DECREMENT</button>
</div>
)
}
}
const actions = {
increment: () => {
return {
type: 'INC',
}
},
decrement: () => {
return {
type: 'DEC',
}
}
};
const AppContainer = connect(
function mapStateToProps(state) {
return {
digit: state
};
},
function mapDispatchToProps(dispatch) {
return bindActionCreators(actions, dispatch);
}
)(First);
ReactDOM.render(
<Provider store={store}>
<AppContainer/>
</Provider>
,document.getElementById('root'))
Here is a working codepen
Very simple code INC and DEC: props and state
Complete Code:
class APP extends Component
{
constructor(props)
{
super(props)
this.state ={
digit: 0
}
this.onIncrement = this.onIncrement.bind(this);
this.onDecrement = this.onDecrement.bind(this);
}
onIncrement()
{
this.setState({
digit: this.state.digit + 1
)}
}
onDecrement()
{
this.setState({
digit: this.state.digit - 1
)}
}
render()
{
return(<p>{this.state.digit}</p>
<button type="button" onClick={this.onIncrement}> + </button>
<button type="button" onClick={this.onDecrement}> - </button>)
}
}
export default APP;

Resources