In my Table component I'm getting data from redux store with useSelector hook.
const info = useSelector(state => {
if (type === 'catalog') {
return store.getState().catalog.products
}
if (type === 'category') {
return store.getState().categories.categories
}
})
Then I'm processing data to correct type
React.useEffect(() => {
if(info.length) {
const prods:any = []
info.forEach((product: any) => {
const productObj: any = {}
productObj._prodid = product?._id
productObj.image = product?.catalogProduct?.image
productObj.category = product?.catalogProduct?.category.name
productObj.name = product?.catalogProduct?.name
productObj.pricePerPiece = product?.catalogProduct?.pricePerPiece
productObj.pricePerPackage = product?.catalogProduct?.pricePerPackage
productObj.address = product?.address
productObj.piecesAtStorage = product?.piecesAtStorage
prods.push(productObj)
})
setData(prods)
}
}, [info])
It takes 3 re-renders.
First rerender - initial data of useState
Second rerender - initial data from useSelector
Third rerender - set data from useSelector into useState
And the output looks like this.
Is it possible to avoid rerender caused by useSelector?
Have you tried shallowEqual function?
import { shallowEqual, useSelector } from 'react-redux'
const selectedData = useSelector(selectorReturningObject, shallowEqual)
The problem is it returns a new array each time it runs. As for object and array, those with same properties/values are technically not same. This is why you are seeing same empty array twice.
You could combine the 2 things in one selector:
const { Provider, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const { createSelector } = Reselect;
const initialState = {
catalog: {
products: [{ _id: 1 }, { _id: 2 }],
categories: [{ _id: 3 }, { _id: 4 }],
},
};
const reducer = (state) => {
return state;
};
//selectors
const selectCatalog = (state) => state.catalog;
//select product or categories and map them
const selectData = createSelector(
[selectCatalog, (_, type) => type],
(catalog, type) => {
const data =
type === 'catalog'
? catalog.products
: type === 'category'
? catalog.categories
: [];
//use map instead of forEach
return data.map((item) => ({
//SO snippet has old babel so removed optional chaining
// you can put it back in your code
_prodid: item._id,
//you can figure out the other props
}));
}
);
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
() => (next) => (action) => next(action)
)
)
);
const App = () => {
const [type, setType] = React.useState('catalog');
const data = useSelector((state) =>
selectData(state, type)
);
console.log('render app', type);
return (
<div>
<select
value={type}
onChange={(e) => setType(e.target.value)}
>
<option value="catalog">catalog</option>
<option value="category">category</option>
</select>
<pre>{JSON.stringify(data, undefined, 2)}</pre>
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/reselect/4.0.0/reselect.min.js"></script>
<div id="root"></div>
Related
I'm learning react context and while developing a todo application using useContext, I'm facing an issue where on submitting one task, the same task gets added two times to an array. The output component would loop through this array and display the results. While debugging I observed that, although the submit of task add only one entry into the array, not sure why and how, the consumer component gets the array with duplicate entry. Please let me know, what I'm missing.
Here is my code of index file that maintains context
import { createContext, useReducer } from "react";
import ContextReducer, { initialState } from "./ContextReducer";
const taskContext = createContext();
const ContextProvider = (props) => {
const [state, dispatch] = useReducer(ContextReducer, initialState);
const setTaskInput = (taskInput) => {
dispatch({
type: "SET_TASKINPUT",
payload: taskInput,
});
};
const addTask = (task) => {
dispatch({
type: "ADD_TASK",
payload: task,
});
};
const deleteTask = (id) => {
dispatch({
type: "DELETE_TASK",
payload: id,
});
};
const todoContext = {
todo: state.todo,
taskInput: state.taskInput,
setTaskInput,
addTask,
deleteTask,
};
return (
<taskContext.Provider value={todoContext}>
{props.children}
</taskContext.Provider>
);
};
export { taskContext };
export default ContextProvider;
This is the code for reducer
const initialState = {
todo: [],
taskInput: "",
};
const ContextReducer = (state = initialState, action) => {
if (action.type === "SET_TASKINPUT") {
state.taskInput = action.payload;
return {
todo: state.todo,
taskInput: state.taskInput,
};
}
if (action.type === "ADD_TASK") {
state.todo = [...state.todo, action.payload];
return {
todo: state.todo,
taskInput: state.taskInput,
};
}
if (action.type === "DELETE_TASK") {
state.todo = state.todo.filter((todo) => todo.id !== action.payload);
return {
todo: state.todo,
taskInput: state.taskInput,
};
}
return state;
};
export { initialState };
export default ContextReducer;
This is the code of output component or say, consumer component
import React, { Fragment, useContext } from "react";
import { taskContext } from "../../Context";
import styles from "./Content.module.css";
const Output = () => {
const { todo, deleteTask } = useContext(taskContext);
const deleteHandler = (e) => {
deleteTask(+e.target.parentElement.parentElement.id);
};
return (
<Fragment>
{todo.length > 0 && (
<div className={styles.outputDiv}>
<ul>
{todo.map((task) => {
return (
<li key={task.id} id={task.id}>
<div className={styles.row1}>{task.task}</div>
<div className={styles.row2}>
<button className={styles.edit}>Edit</button>
<button className={styles.delete} onClick={deleteHandler}>
Delete
</button>
</div>
</li>
);
})}
</ul>
</div>
)}
</Fragment>
);
};
export default Output;
What is the correct way to create redux store within useEffect?
I have a use case that a child App.js will be exported as web-component (e.g. a javascript file), then embed (e.g. reference this file in html header) by a parent react app.
One flow is that the parent app will render App.js twice or N times. I put store creation inside useEffect + empty dependency, so this will be called once, regardless how many times the parent app renders the child app.
Once my store is ready, I render rest of the component + pass the store to context.
In Component.js, I look up the state and want to use them.
The error I have:
https://github.com/facebookincubator/redux-react-hook/blob/master/src/create.ts
const state = store.getState();, getState does not exist. It seems that either store is not ready when I tried to look up the state.
So I wonder what is the correct way to create store within useEffect?
App.js
export default function App() {
const store = useRef(null);
const [storeReady, setStoreReady] = useState(false);
const [token, setToken] = useState('');
useEffect(() => {
store.current = createReduxStore();
setStoreReady(true);
return () => {
store.current = null;
};
}, []);
useEffect(() => {
if (storeReady) {
// get token and set in state
token(auth())
}
}, [auth, storeReady]);
// store.current
if (store.current !== null) {
//test
console.log(
'++++++ store.current not null, condi',
store.current !== null,
store.current
);
return (
<>
<StoreContext.Provider value={store.current}>
<MyApp
token={token}
/>
</StoreContext.Provider>
</>
);
} else {
return null;
}
}
Store.js
export function createReduxStore(initialState = {}) {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(sagaMiddleware))
);
sagaMiddleware.run(rootSaga);
return store;
}
Component.js
export default function MyApp() {
const mapState = useCallback(
state => ({
history:
state !== undefined && state.idv !== undefined ? state.idv.history : [],
status:
state !== undefined && state.idv !== undefined
? state.idv.status
: initialStatus,
error:
state !== undefined && state.idv !== undefined ? state.idv.error : ''
}),
[]
);
const {history, status, error} = useMappedState(mapState);
}
// https://github.com/facebookincubator/redux-react-hook/blob/master/src/create.ts
function useMappedState<TResult>(
mapState: (state: TState) => TResult,
equalityCheck: (a: TResult, b: TResult) => boolean = defaultEqualityCheck,
): TResult {
const store = useContext(StoreContext);
if (!store) {
throw new MissingProviderError();
}
// We don't keep the derived state but call mapState on every render with current state.
// This approach guarantees that useMappedState returns up-to-date derived state.
// Since mapState can be expensive and must be a pure function of state we memoize it.
const memoizedMapState = useMemo(() => memoizeSingleArg(mapState), [
mapState,
]);
// getState is undefined
const state = store.getState();
const derivedState = memoizedMapState(state);
Update 1, but not working #backtick
export default function App() {
const duckStore = { getState() {} };
const store = useRef(duckStore);
const [token, setToken] = useState('');
useEffect(() => {
store.current = createReduxStore();
return () => {
store.current = duckStore;
};
}, []);
// store.current
if (store.current !== null) {
return (
<>
<StoreContext.Provider value={store.current}>
<MyApp />
</StoreContext.Provider>
</>
);
} else {
return null;
}
}
Store.js
export function createReduxStore(initialState = {}) {
const sagaMiddleware = createSagaMiddleware();
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(sagaMiddleware))
);
sagaMiddleware.run(rootSaga);
return store;
}
Component.js
export default function MyApp() {
// There is no store variable here, useMappedState is able to get the store value.
const mapState = useCallback(
state => ({
history:
state !== undefined && state.idv !== undefined ? state.idv.history : [],
status:
state !== undefined && state.idv !== undefined
? state.idv.status
: initialStatus,
error:
state !== undefined && state.idv !== undefined ? state.idv.error : ''
}),
[]
);
const {history, status, error} = useMappedState(mapState);
}
useMappedState calls the store.getState() on every render, even the first one when the store isn't ready. Just duck-type the store ref's initial value:
// module scope
const duckStore = { getState() {} };
// in component
const store = useRef(duckStore);
// in useEffect cleanup
return () => { store.current = duckStore; };
I have a variable const foo: Array<ObjectExt> = useSelector(fooSelector); in a functional component. I want a copy of this variable from the first time the component is loaded that does not change when foo does.
When working with class components, I could simply have const fooCopy = foo.slice(); but that does not work here since the component reloads every time and fooCopy changes.
How do I achieve this in a functional component?
Just useState with the initial value as a copy of foo.
const foo : Array<ObjectExt> = useSelector(fooSelector);
const [origFoo] = useState(foo.slice());
Once origFoo has been initialized, it won't be re-initialized on rerender. You can destructure the setter out if you need to update its value later:
const [origFoo, setOrigFoo] = useState(foo);
// ...
if(someCondition) setOrigFoo(foo.slice())
const {useState} = React;
function App() {
const foo = [new Date().getTime()];
const [origFoo] = useState(foo.slice());
// Just so we have a way to force a rerender
const [count, setCount] = React.useState(0);
return (
<div>
<p>{JSON.stringify(foo)} </p>
<p>{JSON.stringify(origFoo)}</p>
<button onClick={() => setCount(count + 1)}>Update</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<App />,
rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
One solution is to set a flag in local component state. If that flag is fals, then make a copy of the value. Otherwise, don't.
The solution I'd use to accomplish this functionality, is to make a copy of foo, something like initialFoo in the store, and pick it in needed components.
I want a copy of this variable from the first time the component is loaded that does not change when foo does.
When you use useSelector(selector) then react-redux will run selector every time the state changes, if the return value is different than then last time it ran then react-redux will re render the component.
The easiest way of doing this is using a selector that returns only the value it got when called the first time:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = { count: 0 };
//action types
const ADD = 'ADD';
//action creators
const add = () => ({
type: ADD,
});
const reducer = (state, { type, payload }) => {
if (type === ADD) {
return { ...state, count: state.count + 1 };
}
return state;
};
//selectors
const selectCount = (state) => state.count;
//function returning a function
const createSelectInitialCount = () => {
//initialize NONE when createSelectInitialCount is called
const NONE = {};
let last = NONE;
//return the selector
return (state) => {
//check if last value was set
if (last === NONE) {
//set last value (only when called the first time)
last = selectCount(state);
}
return last;
};
};
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const InitialFoo = React.memo(function InitialFoo(props) {
const selectInitialCount = React.useMemo(//can also use useCallback
createSelectInitialCount,//createSelectInitialCount() if you use useCallback
[]
);
const foo = useSelector(selectInitialCount);
return (
<div>
<h3>initialfoo</h3>
<pre>
{JSON.stringify({ ...props, foo }, undefined, 2)}
</pre>
</div>
);
});
const App = () => {
const foo = useSelector(selectCount);
const dispatch = useDispatch();
const [other, setOther] = React.useState(0);
const [showFoo, setShowFoo] = React.useState(true);
const remountFoo = () => {
setShowFoo(false);
Promise.resolve().then(() => setShowFoo(true));
};
return (
<div>
<button onClick={() => dispatch(add())}>
foo:{foo}
</button>
<button onClick={() => setOther((o) => o + 1)}>
other{other}
</button>
<button onClick={remountFoo}>remount Foo</button>
{showFoo && <InitialFoo other={other} />}
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>
Another way is to create a pure component using React.memo with a custom compare function that ignores foo:
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = { count: 0 };
//action types
const ADD = 'ADD';
//action creators
const add = () => ({
type: ADD,
});
const reducer = (state, { type, payload }) => {
if (type === ADD) {
return { ...state, count: state.count + 1 };
}
return state;
};
//selectors
const selectCount = (state) => state.count;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (next) => (action) =>
next(action)
)
)
);
const InitialFoo = React.memo(
function InitialFoo(props) {
return (
<div>
<h3>initialfoo</h3>
<pre>{JSON.stringify(props, undefined, 2)}</pre>
</div>
);
},
//custom compare function when returning false
// component will re render
({ other }, { other: newOther }) => {
return other === newOther;
}
);
const InitialFooContainer = (props) => {
const foo = useSelector(selectCount);
//store foo in ref, will never change after mount
const fooRef = React.useRef(foo);
const newProps = { ...props, foo: fooRef.current };
return <InitialFoo {...newProps} />;
};
const App = () => {
const foo = useSelector(selectCount);
const dispatch = useDispatch();
const [other, setOther] = React.useState(0);
const [showFoo, setShowFoo] = React.useState(true);
const remountFoo = () => {
setShowFoo(false);
Promise.resolve().then(() => setShowFoo(true));
};
return (
<div>
<button onClick={() => dispatch(add())}>
foo:{foo}
</button>
<button onClick={() => setOther((o) => o + 1)}>
other{other}
</button>
<button onClick={remountFoo}>remount Foo</button>
{showFoo && (
<InitialFooContainer foo={foo} other={other} />
)}
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>
I have a small component that renders another page, the webpage URL has a token attached as an URL parameter, like in the sample bellow:
const SampleComponent = () => {
const { refreshToken } = useSelector(state => state.auth);
const src = `${HOSTNAME}/page/?refresh_token=${refreshToken}`;
return <webview src={src} />;
};
export default SampleComponent;
I have a special cron that runs every hour and updates the tokens and Redux is updated as well with the new tokens.
window.tokensCron = new CronJob('0 0 * * *', () => {
store.dispatch(getTokens());
});
When the token is updated in Redux the page is being refreshed automatically.
How to prevent updating the component so that the refresh page won't happen?
So you want to use the token from redux state only when the component mounts?
You can make a custom hook that sets the token only once after the component mounts by deliberately leaving out a dependency of an effect, then use that in a HOC to pass the value of the token as it was when it mounted with other props to the component that needs the token:
//custom hook gets token only on mount
const useToken = () => {
const token = useSelector(selectToken);
const [val, setVal] = useState();
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => setVal(token), []);
return val;
};
//hoc that will only re render if props change (not when token changes)
const withToken = (Component) => (props) => {
const token = useToken();
const propsWithToken = useMemo(
() => ({ ...props, token }),
[props, token]
);
return token ? <Component {...propsWithToken} /> : null;
};
Make sure that the component you pass to withToken is a pure component so it won't get re rendered when props passed to it won't change.
Code snippet with this example is below.
const { Provider, useDispatch, useSelector } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const {
useState,
useRef,
useEffect,
memo,
useMemo,
} = React;
const initialState = {
token: 1,
};
//action types
const REFRESH_TOKEN = 'REFRESH_TOKEN';
//action creators
const refreshToken = () => ({
type: REFRESH_TOKEN,
});
const reducer = (state = initialState, { type }) => {
if (type === REFRESH_TOKEN) {
return {
...state,
token: state.token + 1,
};
}
return state;
};
//selectors
const selectToken = (state) => state.token;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(() => (n) => (a) => n(a))
)
);
//custom hook gets token only on mount
const useToken = () => {
const token = useSelector(selectToken);
const [val, setVal] = useState();
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => setVal(token), []);
return val;
};
//hoc that will only re render if props change (not when token changes)
const withToken = (Component) => (props) => {
const token = useToken();
const propsWithToken = useMemo(
() => ({ ...props, token }),
[props, token]
);
return token ? <Component {...propsWithToken} /> : null;
};
const Component = ({ token }) => {
const r = useRef(0);
r.current++;
return (
<div>
rendered: {r.current} token: {token}
</div>
);
};
//using React.memo to make Component a pure component
const PureWithToken = withToken(memo(Component));
const App = () => {
const token = useSelector(selectToken);
const [toggle, setToggle] = useState(true);
const dispatch = useDispatch();
//refresh token every second
useEffect(() => {
const interval = setInterval(
() => dispatch(refreshToken()),
1000
);
return () => clearInterval(interval);
}, [dispatch]);
return (
<div>
<div>token:{token}</div>
<label>
Toggle component with token
<input
type="checkbox"
checked={toggle}
onChange={() => setToggle((t) => !t)}
/>
</label>
{/* when component re mounts it will have the newest token */}
{toggle ? <PureWithToken /> : null}
</div>
);
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>
Your state may be malformed.
As I see you have:
a token that updates frequently
an initial token, which is the first token value, and never change
So consider modify your state to follow this structure:
state = {
auth: {
initialToken,
refreshToken
}
};
Then in your component, simply do that:
const initialToken = useSelector(state => state.auth.initialToken);
Important, in your useSelector please returns only the value you want (your token, not the whole auth). Like that your component will update ONLY if your token changes.
As you do in your current code if auth changes, your component is updated even if token did not change.
I have to use useDispatch() for my toggle buttons so I have to refractor them from react to redux state. I was following the tutorial of basics of Redux and I think I have done that properly but when I try to at least useSelector to display the redux'state of button it doesnt show anything.
So here is my code:
// types.js in actions folder
export const TOGGLE = "TOGGLE";
// buttonActions in actions folder
export const toggle = () => {
return {
type: 'TOGGLE'
};
};
// buttonReducer in reducers folder
const buttonReducer = (state = true, action) => {
switch(action.type) {
case 'TOGGLE':
return !state;
default:
return state;
};
};
export default buttonReducer;
And the buttonReducer is imported into combineReducers which go to store.
The component code:
import React, { useState, useEffect } from 'react'
import isloff from './mainpage_imgs/isloff.png'
import islon from './mainpage_imgs/islon.png'
import PropTypes from "prop-types";
import { connect, useDispatch, useSelector } from "react-redux";
import { toggle } from '../../actions/buttonActions'
const Islbutton = props => {
const [open, setOpen] = useState(true);
const [role, setRole] = useState('');
useEffect(() => {
if (props.auth.user)
{
setRole(props.auth.user.role);
}
}, []);
const test = useSelector(state => state.button);
const checkRole = (role) => {
if (role === 'Menager' || role === 'Technolog')
{
return true }
else
{
return false
};
}
const toggleImage = () => {
if(checkRole(role)) {
setOpen(!open)
};
}
const getImageName = () => open ? 'islOnn' : 'islOfff'
const dispatch = useDispatch();
return(
<div>
<img style={islplace} src={open ? islon : isloff }
onClick={()=> dispatch(toggle())} />
</div>
);
}
Islbutton.propTypes = {
button: PropTypes.func.isRequired,
auth: PropTypes.obj.isRequired
};
const mapStateToProps = state => ({
button: state.button,
auth: state.auth
});
export default connect(mapStateToProps, {}), (Islbutton);
Based on your latest comments and my understanding of your use case I may suggest following distilled approach:
//dependencies
const { render } = ReactDOM,
{ createStore } = Redux,
{ connect, Provider } = ReactRedux
//action creators
const SET_ROLE = 'SET_ROLE',
MANAGER_APPROVED = 'MANAGER_APPROVED',
setRole = role => ({type:SET_ROLE, role}),
mngAppr = () => ({type:MANAGER_APPROVED})
//initial state, reducer, store
const initialState = {role:'Technolog', approved:false},
appReducer = (state=initialState, action) => {
switch(action.type){
case SET_ROLE : {
const {role} = state,
{role: newRole} = action
return {...state, role: newRole}
}
case MANAGER_APPROVED : {
const {approved} = state
return {...state, approved: !approved}
}
default: return state
}
},
store = createStore(appReducer)
//ui component to emulate toggling roles
const SwitchRoles = ({currentRole, switchRole}) => (
<div>
<label><input type="radio" name="role" value="Manager" onChange={e => switchRole(e.target.value)} />Manager</label>
<label><input type="radio" name="role" value="Technolog" onChange={e => switchRole(e.target.value)} />Technolog</label>
</div>
)
//connect radio buttons click to togling roles action
const mapDispatch = dispatch => ({switchRole: role => dispatch(setRole(role))}),
SwitchRolesContainer = connect(null,mapDispatch)(SwitchRoles)
//ui component to toggle 'approved' within global state
const ToggleApprove = ({onApprove,isManager}) => (
<button onClick={onApprove} disabled={!isManager}>Toggle</button>
)
//connect onToggle handler to dispatching 'toggle' action
const mapStateToProps = ({role}) => ({isManager: role == 'Manager'}),
mapDispatchToProps = dispatch => ({onApprove: () => dispatch(mngAppr())}),
ToggleApproveContainer = connect(mapStateToProps, mapDispatchToProps)(ToggleApprove)
//ui component to display current state of 'open'
const IsApproved = ({isApproved}) => <div>{isApproved ? 'Approved by manager' : 'Not approved by manager'}</div>
//attach isOpen prop to global 'open' variable
const mapState = ({approved}) => ({isApproved: approved}),
IsApprovedContainer = connect(mapState)(IsApproved)
//render the app
render (
<Provider store={store}>
<SwitchRolesContainer />
<IsApprovedContainer />
<ToggleApproveContainer />
</Provider>,
document.getElementById('root')
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.1.3/react-redux.min.js"></script><div id="root"></div>
Hopefully, it gives a piece of mind about toggling global variables and mapping their values onto local components state.