I have a unique situation in which my <Provider> component (and entire Redux Store) is exported from a middleware application to multiple front end React apps. The middleware has its own set of reducers but the client apps can inject their own reducers into the store when they call the provider.
It is now being asked that I accept an initial state (preloadedState) object when the Provider is called so that the initial state of the app can be loaded with dynamic initial state. This object will be an arbitrary set of state data (with corresponding reducers) so I'll have the data structures correctly with the shape in the reducers, but I won't know what values they're sending.
Here's the basic ReduxStore set up, changed here for simplicity:
ReduxStore.ts
import { configureStore } from '#reduxjs/toolkit';
import { ExampleReducer } from './slices/ExampleSlice';
export const reducer = {
example: ExampleReducer
};
const ReduxStore = configureStore({
middleware: ...,
reducer,
});
export default ReduxStore;
CoreProvider.tsx
import React from 'react';
import { Provider } from 'react-redux';
import { combineReducers, ReducersMapObject, AnyAction } from '#reduxjs/toolkit';
import ReduxStore, { reducer } from './ReduxStore';
export type ConfigProps = {
newReducers?: ReducersMapObject<unknown, AnyAction>;
preloadedState: Object;
};
const CoreProvider: React.FC<ConfigProps> = ({
children,
newReducers,
}) => {
const newReducer = combineReducers({ ...newReducers, ...reducer });
ReduxStore.replaceReducer(newReducer);
return <Provider store={ReduxStore}>{children}</Provider>;
};
export default CoreProvider;
index.jsx
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<CoreProvider newReducers={{ arbitrary: arbitraryReducer }}>
<App />
</CoreProvider>
</React.StrictMode>,
);
Now, I'm aware of the reasons I should NOT do this, and I am aware that initial state comes from the reducers themselves, which is handled when the reducers are merged with the default reducers when the Provider is called.
I know that configureStore accepts a preloadedState option but where I'm stuck is how to dynamically pass a preloadedState coming in as a prop from the Provider component. I have tried wrapping the configureStore call inside a function that is called within the Provider component, which does indeed set the initial state, but my ReduxStore export within ReduxStore.ts is undefined due to the way the app loads and the configureStore is called.
Provider calling configureStore with preloadedState props:
import { initReduxStore, reducer } from './ReduxStore';
const CoreProvider: React.FC<ConfigProps> = ({
children,
newReducers,
preloadedState
}) => {
const newReducer = combineReducers({ ...newReducers, ...reducer });
const ReduxStore = initReduxStore(preloadedState, newReducer);
return <Provider store={ReduxStore}>{children}</Provider>;
};
export default CoreProvider;
ReduxStore with exported configureStore function and undefined ReduxStore export:
export const reducer = {
example: ExampleReducer
};
let ReduxStore;
export const initReduxStore = (incomingState, incomingReducer) => {
if (ReduxStore) return ReduxStore;
const store = configureStore({
middleware: ...,
preloadedState: incomingState,
reducer: incomingReducer,
});
ReduxStore = store;
return ReduxStore;
};
export default ReduxStore; // This becomes undefined because the file loads before the Provider component calls the initReduxStore function
The ReduxStore export is undefined because the file loads before the Provider component calls the initReduxStore function and ReduxStore is undefined at the time it's exported.
Is there a known way that I have overlooked to easily set the preloaded state object when the provider is called? Should I restructure how the ReduxStore is created?
In short terms, how do I get the preloadedState prop from my Provider to my configureStore, while still exporting the ReduxStore to the rest of the app?
export const ReduxStore = configureStore({
preloadedState,
reducer,
});
// HOW DO I CONNECT THESE
const CoreProvider: React.FC<ConfigProps> = ({
children,
newReducers,
preloadedState
}) => {
return <Provider store={store}>{children}</Provider>;
};
export default CoreProvider;
Any thoughts are appreciated.
The short answer is, you can't. The preloadedState option can only be passed in when you call configureStore(). Once the store is created, the only way to update the store state is to dispatch an action.
On top of that, behavior such as replacing the reducer or dispatching actions would qualify as a side effect, and React components must not have side effects directly in the rendering logic.
The closest suggestion I have would be a useLayoutEffect hook in this component that watches for changes to the provided reducers or state, and does the store.replaceReducer() call.
Also, you could have a wrapping reducer that watches for some kind of a "merge in this additional state" action, and returns the updated state with the additional fields.
But overall, this is a very unusual use case, and not something Redux is really designed for.
Related
I am implementing redux-undo library introduced in redux docs in my RTK project. The example in the docs guides you to wrap single slice (or reducer) to make it 'undoable'.
// todos.js
import undoable from 'redux-undo'
/* ... */
const todos = (state = [], action) => {
/* ... */
}
const undoableTodos = undoable(todos)
export default undoableTodos
import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
const todoApp = combineReducers({
todos,
visibilityFilter
})
export default todoApp
You may wrap one or more reducers in undoable at any level of the reducer composition hierarchy. We choose to wrap todos instead of the top-level combined reducer so that changes to visibilityFilter are not reflected in the undo history.
This example, as described above, only restores changes in todos slice. Anything happened to visibilityFilter is not undoable.
What I want is to wrap the entire store with undoable() so that with one undo() function call, you can revoke all changes to the store.
Below is my attempt in redux toolkit and I want to know if this is the right way to go.
import { configureStore } from "#reduxjs/toolkit";
import undoable from 'redux-undo'
import todoSlice from "../features/todo/todoSlice";
import visibilityFilterSlice from "../features/visibilityFilter/visibilityFilterSlice";
const store = configureStore({
reducer: {
todos: todoSlice,
visibilityFilter: visibilityFilterSlice,
},
});
export default undoable(store);
No, your attempt wraps the store instance, not a reducer - redux-undo won't know what to make of that. Try this instead (using combineReducers with RTK is totally fine):
const rootReducer = combineReducers({
todos: todoSliceReducer,
visibilityFilter: visibilityFilterSliceReducer,
})
const undoableRootReducer = undoable(rootReducer)
const store = configureStore({
reducer: undoableRootReducer,
});
For TypeScript it is important not to do reducer: undoable(rootReducer),, but to do this in a variable declaration above the configureStore call.
I created a create react app and included redux with card lists and a searchbox that displayed the filtered results, the app was working before I added redux but now it isn't returning any results. When I console.log(this.props.store) it is returning undefined. I would really appreciate it if someone can help me with this. My files are as below:
constants.js
export const CHANGE_SEARCH_FIELD = 'CHANGE_SEARCH_FIELD';
actions.js
import {CHANGE_SEARCH_FIELD} from './constants.js';
export const setSearchField = (text) => ({
type: CHANGE_SEARCH_FIELD,
payload: text
})
reducer.js
import {CHANGE_SEARCH_FIELD} from './constants.js';
const intialState = {
searchField: ''
}
export const searchTeacher = (state=intialState, action={}) => {
switch(action.type) {
case CHANGE_SEARCH_FIELD:
return Object.assign({}, state, { searchField: action.payload });
default:
return state;
}
}
index.js
import ReactDOM from 'react-dom';
import './index.css';
import {Provider} from 'react-redux';
import {createStore} from 'redux';
import App from './App.js'; //Our main parent component
import {searchTeacher} from './reducer.js';
import 'tachyons';
import * as serviceWorker from './serviceWorker';
const store = createStore(searchTeacher)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root') );
serviceWorker.unregister();
App.js
import React, {Component} from 'react';
import {connect} from 'react-redux';
import CardList from './CardList.js';
import {teacher} from './teacher.js';
import Searchbox from './searchbox.js';
import ErrorBoundry from './ErrorBoundry';
import Scroll from './Scroll.js';
import './App.css';
import {setSearchField} from './actions.js';
const mapStateToProps = state => {
return {
searchField: state.searchField
}
}
const mapDispatchToProps = (dispatch) => {
return {
onSearchChange: (event) => dispatch(setSearchField(event.target.value))
}
}
class App extends Component {
constructor(){
super()
this.state = {
teacher: teacher, //teacher: [],
}
}
render(){
console.log(this.props.store);
const { searchField, onSearchchange } = this.props;
const filteredteacher= teacher.filter(
teacher =>{
return teacher.name.toLowerCase().includes(searchField.toLowerCase());
});
return(
<div className="tc">
<h1 className="f1"> Faculty Members ! </h1>
<Searchbox searchChange={onSearchchange} />
<Scroll>
<ErrorBoundry>
<CardList teacher={filteredteacher} />
</ErrorBoundry>
</Scroll>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
There won't be any props.store, because none of your code is passing down a prop named store to that component.
Components that have been wrapped in connect get props from three sources, combined:
Props passed from the parent component
Props returned from mapState
Props returned from mapDispatch
In this case, mapState is returning {searchField}, and mapDispatch is returning {onSearchChange}, and there's no props from the parent. So, the combined props are {searchField, onSearchChange}.
As a side note, you should use the "object shorthand" form of mapDispatch instead of writing it as a function:
const mapDispatch = {onSearchChange: setSearchField};
You will get two props from redux according to your code,
this.props.searchField
this.props.onSearchChange
connect function of react-redux used to connect react and redux.
mapDispatch is used to dispatch your actions which hold the payload(Second argument of connect function)
mapState is used to get the state of your properties(First argument of connect function)
So in your code, there is not any prop named store, Store is a global redux state which you can get with this method Store.getState() but here is store is redux store which you are passing here const store = createStore(searchTeacher) in your index.js file, This will show whole state of the redux store.
here is how you can get the state of your store.
How do I access store state in React Redux?
You will dispatch an action named onSearchChange like below in your on change method.
this.props.onSearchChange(e)
and redux will return you a value of this after storing in reducer with the name of this.props.searchField.
this.props.store would only be accessible if it was passed down from a parent component (which you are not doing here)
You create your store in index.js but you are not exposing an interface to it.
const store = createStore(searchTeacher);
You can expose these functions from your index.js file to reference the store:
export const getStore = () => store;
export const getState = () => { return store.getState(); };
Then from anywhere else (although not good practice):
import { getStore, getState } from 'index.js';
I'm implementing immutable on my react project, using it with redux, making state an immutable object using fromJS() function (from immutable library). In my reducer file, everything works, I receive an action, I can set the new value using setIn() function, and I can use getIn() function to access state.
But when I get state from connect() function, using mapStateToProps, even if console.log shows an apparently immutable object, I can't use immutable functions, like toJs() or getIn() here. I receive always this error: TypeError: state.getIn is not a function.
My index.js file
import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'redux';
import PropTypes from 'prop-types';
import Button from '#material-ui/core/Button';
import { template as templateAction } from './actions';
import withReducer from '../../../reducer/withReducer';
import templateReducer from './reducer';
export const Template = ({ name, template }) => (
<Button onClick={template}>
{name}
</Button>
);
Template.propTypes = {
name: PropTypes.string.isRequired,
template: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => {
console.log('state is equal to', state);
return (
{
name: state.getIn(['templateReducer', 'name']),
});
};
export const mapDispatchToProps = (dispatch) => ({
template: () => dispatch(templateAction()),
});
export default compose(
withReducer('templateReducer', templateReducer),
connect(mapStateToProps, mapDispatchToProps),
)(Template);
Result of console.log(state)
Result of console.log(state)
PS: When I don't use immutable state, everything works well.
Looks like state inside mapStateToProps function is an object with one property of 'templateReducer' that has a value of type Map.
I'm a bit rusty with my React knowledge, but maybe sharing the code for templateReducer would be helpful.
What does withReducer('templateReducer', templateReducer) do with the reducer function?
Seems like it is setting the state from the reducer to the key templateReducer before sending it to the mapStateToProps function. (maybe??)
Probably, changing this function to access the State before using immutable methods will remove the error.
export const mapStateToProps = (state) => {
console.log('state is equal to', state);
return (
{
name: state['templateReducer']?.getIn(['name']),
});
};
Use connect from griddle-react not react-redux.
Given that in a file called goose.js I have exported a reducer as such:
//reducer
const reducer = handleActions(
{
[setExchangeRate]: (state, { exchangeRate }) => ({
...state,
exchangeRate
}),
[setExchangeCurrency]: (state, { exchangeCurrency }) => ({
...state,
exchangeCurrency
}),
[setBaseCurrency]: (state, { baseCurrency }) => ({
...state,
baseCurrency
})
},
initialState
);
export default reducer;
const selectCurrencyExchange = state => state[MODULE_NAME];
and in another file called index.jas, I try to import the reducer and assigned it to a provider:
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import reducer from "./redux/goose";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={reducer}>
<App />
</Provider>,
rootElement
);
is there a reason why I am getting
TypeError store.getState is not a function error? Is it illegal to do this?
The dependencies I am using is react 16.8.2, react-redux 5.07, redux, redux actions, redux-define.
I welcome an example that allows me to use the reducer structure in goose.js
react-redux Provider is taking a store not a reducer.
You need to create the store with use of createStore function from redux library and pass to it your reducer. And after that you can pass the created store to the Provider.
Have a look at the basic example in redux documentation.
This might be a really silly one, but I was hanging my head around it for a while. I have a React project ( with Redux ). In mapStateToProps, state value is coming as undefined if I try to access the state directly as
const mapStateToProps = state => ({ data: state.data });
Instead, I always have to specify my reducer name ( the reducer which handles this particular state in it ) to access the state value :
const mapStateToProps = state => ({ data: state.customReducer.data });
Here is my code :
import { combinedReducer } from 'redux;
import customReducer from './customReducer';
const rootReducer = combineReducer({
customReducer
});
export default rootReducer;
customReducer.js : as follows
const initialState = {};
const customReducer = ( state = initialState, action ) => {
const { type, payload } = action;
switch (type) {
case 'SOME_ACTION':
return {
...state,
data: payload.data
}
break;
default:
return state;
}
}
export default customReducer;
store.js
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
const configStore = () => {
return createStore (
rootReducer,
applyMiddleware(thunk)
);
}
const store = configStore();
export default store;
Does anyone know what is going wrong with the implementation ? Or is it the way to access different state values from different store ?
How can I directly access any state variable as
data: state.`state_value` , instead of , data : state.`reducerName`.`state_value` ?
Any help on this would be much appreciated.
There's nothing wrong with your implementation, this is just the way that combineReducers works. The reducer names are used to partition your store (e.g. Everything in your users reducer will be under state.users).
If you don't want to have to use the name of the reducer in your mapStateToProps, you can use selectors. However, in the selector, you will still have to use state.reducerName.
If you really don't want to have to go through more than one layer to get to the value you want, you could create a separate reducer for each value (i.e. In your example, the reducer would be named data). This obviously isn't the preferred way of doing it though.
Well, combineReducers isn't really using the reducer name, but the property name you specify for it. If you do:
const rootReducer = combineReducer({
foo: customReducer,
});
You'll be able to access the data at state.foo.data.