Connecting Redux Stores using Redux Toolkit - javascript

for various reasons, my workplace is currently trying to introduce React/Redux into our project. Our project utilizes Shopify and it's Liquid templates along with jQuery. They want to migrate towards React, and we have thus far been injecting React by looking for specific ID's and injecting React Components in that way.
Due to this, and because we are now needing a store to keep and render data, I've come to a strange issue. If I wrap each one of these injected components with a Provider and store, I can essentially have each component with it's own store, but that doesn't help at all as it's pretty much mimicking local state.
Is there a way for me to 'connect' and share the store with multiple components?
I thought about wrapping the whole project, as is usually the case, but doing so would/should render the Liquid useless.
Here's an example of what's going on:
import ReactDOM from "react-dom";
import React from "react";
import attrToProps from "../../../partials/data-attribute-to-props.js";
import FavoritesToggler from "./index.js";
import store from "./../../Store/store"
import { Provider } from "react-redux"
const rootEl = document.getElementById("fav-container-react");
if(rootEl) {
const props = attrToProps(rootEl.attributes);
rootEl && ReactDOM.render(<Provider store={store}><FavoritesToggler {...props} /></Provider>, rootEl);
}
This is injected into a div that contains the 'fav-container-react' id.
But we have several of these at the moment, and I'm wondering how to get them to connect all to the same store.
Any ideas would be appreciated, including perhaps a possible change in architecture (we're required to 'show progress' in order to continue getting funded, so starting a new project isn't an option. Meaning I need a solution that is able to continuously update legacy code to React)

You don't need a provider if the component has a store prop and you use connect. The hooks won't work so I'd advise using a containers that can be refactored using the hooks when all of the project is converted to React.
const { connect } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = { counter: 0 };
//action types
const UP = 'UP';
//action creators
const up = () => ({
type: UP,
});
const reducer = (state, { type }) => {
if (type === UP) {
return { ...state, counter: state.counter + 1 };
}
return state;
};
//selectors
const selectCounter = (state) => state.counter;
//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 = ({ count, up }) => {
return <button onClick={up}>{count}</button>;
};
const AppContainer = connect(
(state) => ({
count: selectCounter(state),
}),
{ up }
)(App);
const OtherContainer = connect(
(state) => ({
count: selectCounter(state),
})
)(({count})=><h1>{count}</h1>);
ReactDOM.render(
<AppContainer store={store} />,
document.getElementById('root-1')
);
ReactDOM.render(
<AppContainer store={store} />,
document.getElementById('root-2')
);
ReactDOM.render(
<OtherContainer store={store} />,
document.getElementById('root-3')
);
<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-1"></div>
<div id="root-2"></div>
<div id="root-3"></div>

Related

UseSelector State is Undefined

The page does not render, citing TypeError: state is undefined, tracing back to this line in SelectForm.js: const filter = useSelector(state => state.filter);.
I've spent hours trying to figure out what I'm doing wrong. I've tried createSelector but that didn't work. I've tried dispatching a "Fetch Initial State" action, and that didn't work. The component is wrapped in provider tags. I'm not sure why I don't have access to the state. At this point I'm unable to see any flaws I've been looking at it for so long.
Code Snippets
reducer.js
let initialState = {
filter: {
country: null,
state: null,
level: null,
team: null
},
isDBConnecting: false,
isDBConnected: false,
isDBError: false
}
const SelectorReducer = (state=initialState, action) => {
switch (action.type) {
case 'DB_CONNECT_INIT':
return {
...state,
isDBConnecting: true,
isDBConnected: false,
isDBError: false,
};
...
...
}
export default SelectorReducer;
actions.js
export const initializeDBConnection = () => {
return {
type: 'DB_CONNECT_INIT'
}
};
ParentComponent.js
import React from 'react';
import { createStore } from 'redux';
import { Provider } from 'react-redux'; //import provider to provide component access to the state
//Component imports
import SelectForm from './components/SelectForm'
import SelectorReducer from '.../reducer.js'
const SelectorStore = createStore(SelectorReducer);
const ParentComponent = () => {
return (
<div className="page-container">
<div id="carousel">
<div id="wrapper">
<Provider store={SelectorStore}>
<SelectForm />
</Provider>
</div>
</div>
</div>
)
}
SelectForm.js (Child Component, wrapped in Provider tags above)
//IMPORTS
import React from 'react'; //import react
import { useSelector, useDispatch } from 'react-redux';
//COMPONENT IMPORTS
import FormGroup from '../FormGroup';
import { * as actions } from '.../actions.js';
const SelectForm = (props) => {
//STATEFUL IMPORTS
//filter
const filter = useSelector(state => state.filter);
Credit to #NicholasTower for the answer in the comments. My reducer did not have a default case in which
default: return state
Putting that in solved the issue.
let filter = useSelector(state => {
console.log('State: ', state);
return state.pieChart.filter;
});
Make sure that you are using the correct object for state.
Add a console for debugging and check the state object. Many times we use multiple reducers which makes our component state nested in global object.
We should import useSelector form react-redux and then select user details form store in the following way
import { useSelector } from 'react-redux'
const User = () => {
const userInfo= useSelector(state => state.user)
return <div>{userInfo.name}</div>
}
Inside ParentComponent.js you are saying <Provider store={TeamSelectorStore}>. I think you meant to say <Provider store={SelectorStore}>
In my case I was storing the data coming from Redux state at didMount stage whereas the Redux states weren't fully uploaded yet.
const currentUser = useAuth();
useEffect(() => {
form.setFieldValue('email', currentUser.email);
}, []);
Adding currentUser to useEffect dependency array, get it resolved for me:
useEffect(() => {
form.setFieldValue('email', currentUser.email);
}, [currentUser]);
Sometimes having a break in the switch block of the reducer can cause the prop not defined in the state.
This error can also happen because of an uppercase / lowercase mistake.
let horns = useSelector(state => state.largeRhinohorns);
let horns = useSelector(state => state.largeRhinoHorns);

Test a react component using test and react-testing-library

I just joined a team where we use react, redux, recompose to construct components to build UI. There aren't any unit tests in the application and there isn't consistent architecture for the application. I decided to take it upon myself to add unit tests using jest and react-testing-library. I succeed with few snapshot tests but I am struggling with unit testing. I am still learning react and pretty new to redux. I would love some suggestion. I am going to share a component which renders a table with column and row. I would love a feedback.
import React, { useEffect, useState } from 'react';
import { compose } from 'recompose';
import { connect } from 'react-redux';
import { clearAll, fetchContacts } from '~/store/resources/contacts/actions';
import { isDevEnv } from '~/utils';
import Sidebar from './Sidebar';
import Table from './Table';
import Toolbar from './Toolbar';
const Contacts = ({ clearAll, fetchContacts, ...props }) => {
const [searchValue, setSearchValue] = useState('');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [canonicalFormValues, setCanonicalFormValues] = useState({ active: true });
useEffect(() => {
fetchContacts();
return () => {
clearAll();
};
}, []);
const closeSidebar = () => {
if (isDevEnv) {
console.log('hit close function');
}
setIsSidebarOpen(false);
};
return (
<div>
<Toolbar
searchValue={searchValue}
setSearchValue={setSearchValue}
setIsSidebarOpen={setIsSidebarOpen}
/>
<Table setCanonicalFormValues={setCanonicalFormValues} />
<Sidebar
isSidebarOpen={isSidebarOpen}
closeSidebar={closeSidebar}
canonicalFormValues={canonicalFormValues}
/>
{isDevEnv && (
<div>
This is coming from the contact folder
<br />
state values:
<br />
{JSON.stringify({ searchValue })}
<br />
{JSON.stringify({ isSidebarOpen })}
<br />
{JSON.stringify({ canonicalFormValues })}
</div>
)}
</div>
);
};
const mapDispatchToProps = {
clearAll,
fetchContacts,
};
export default compose(
connect(
null,
mapDispatchToProps,
),
)(Contacts);
I generally start out with a simple "should render without crashing" test. I prefer to export and test the undecorated component, in your case Contacts.
export const Contacts = ({ clearAll, fetchContacts, ...props }) => { ...
In the test file
import React from 'react';
import { render } from '#testing-library/react';
import { Contacts } from '.';
// mock the other imported components, they should already be tested alone, right?
jest.mock('./Sidebar');
jest.mock('./Table');
jest.mock('./Toolbar');
describe('Contacts', () => {
it('should render without crashing', () = {
render(
<Contacts
// pass all the props necessary for a basic render
clearAll={jest.fn()}
fetchContacts={jest.fn()}
/>
);
});
});
At this point I run a code coverage report to see how much I have, then add more tests with varying prop values and/or using the react-testing-library's matchers to target buttons or elements to assert text is visible or trigger callbacks, etc, until I have the coverage I want.
Sometimes some of your components may rely on context provider, and in this case RTL allows you to specify wrappers. For example if your component gets decorated with react-intl for string localization, you can provide a wrapper.
export const Contacts = ({ clearAll, fetchContacts, intl }) => { ...
...
export default compose(
connect(
null,
mapDispatchToProps,
),
injectIntl,
)(Contacts);
Create a wrapper
import { IntlProvider } from 'react-intl';
const IntlWrapper = ({ children }) => (
<IntlProvider locale="en">{children}</IntlProvider>
);
const intlMock = {
...
formatMessage: message => message,
...
};
and to test, specify the wrapper in the render options argument
render(
<Contacts
// pass all the props necessary for a basic render
clearAll={jest.fn()}
fetchContacts={jest.fn()}
intl={intlMock}
/>,
{
wrapper: IntlWrapper
}
);
react-testing-library has a lot of documentation, but it is worth reading through. Hope this helps you get going.

How to update React component after changing state through redux?

I was learning React and Redux and while doing that I decided to make webpage with a button which on clicking would change the state. Below the button I wanted to display the current state in a different component. Though the button on clicking changes the state, but it is not getting reflected in the component. Here is my code:
App.js
import React from 'react'
import Name from './Name'
import {changeName} from './Action';
export default function App () {
return (
<div>
<button onClick={changeName}>Click me</button>
<Name />
</div>
)
}
Name.js
import React from 'react'
import {store} from './Store'
function Name(props) {
return (
<div>
My name is: {store.getState()}
</div>
)
}
export default Name
Store.js
import { createStore } from 'redux';
import {reducer} from './Reducer';
export const store = createStore(reducer, 'Tarun');
Action.js
import {store} from './Store';
export const changeName = () => {
if (store.getState() === "Tarun"){
store.dispatch({ type: 'name', payload: 'Subhash' });
}
else{
store.dispatch({ type: 'name', payload: 'Tarun' });
}
}
Reducer.js
export const reducer = function(state, action) {
if (action.type === 'name') {
return action.payload;
}
return state;
};
When I click the button, The text inside the Name component does not change. What is the issue?
You need to set up your reducer and initial store properly following the Redux documentation.
You're missing a Provider, which will provide your store to your application.
const store = createStore(reducer, applyMiddleware(thunk));
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
Now, your store is available to your components.
Your reducer needs an initial state too and you're always supposed to return an updated copy of your state. That said, don't change the state directly, but make a copy, change it, then return that copy.
const initialState = {
name: ""
};
const reducer = function(state = initialState, action) {
if (action.type === "name") {
return { ...state, name: action.payload };
} else {
return state;
}
};
export default reducer;
You might have noticed that I added a middleware to your store, and that's because it's usually the way to go when accessing your current reducer's state in your actions. That said, I installed redux-thunk for that, so in your action, you can have something like this:
export const changeName = () => {
return (dispatch, getState) => {
if (getState().name === "Tarun") {
dispatch({ type: "name", payload: "Subhash" });
} else {
dispatch({ type: "name", payload: "Tarun" });
}
};
};
Now, with your store being provided to your app, your reducer being done and your actions being ready to go, you can connect different components to your reducer.
You use the high order component in react-redux called connect for that. For example, in your Name component, we can connect the name to be displayed to your reducer by mapping your state to the component's props:
function Name(props) {
return <div>My name is: {props.name}</div>;
}
const mapStateToProps = state => {
return {
name: state.name
};
};
export default connect(mapStateToProps)(Name);
The nice thing here is that you can also leave the first parameter in the connect high order component empty and just pass the second, which would be the dispatch functions. Well, that's what you would do in your App component, you would connect it to the changeName action.
function App(props) {
return (
<div>
<button onClick={props.changeName}>Click me</button>
<Name />
</div>
);
}
const mapDispatchToProps = dispatch => {
return {
changeName: () => dispatch(changeName())
};
};
export default connect(
null,
mapDispatchToProps
)(App);
Now, when App dispatches a changeName action, your reducer state will be updated and the other components that are connected to the reducer's state will re-render.
Summary: Try to think of your store as an empty jar of candies. Your jar starts empty, but different actions could change what's inside the jar. On top of that, different people in the house that know where the jar is can go get some candy. Translating to your problem, your app begins with an empty name and you have an action that sets up a name. The components that know where to find that name by being connected to your reducer will know when that name changes and will get the updated name.
The final code can be found here:
The only way your name component will rerender is its props or state change, or if a parent component rerenders. Making a change in redux will not automatically do this. In order to see changes to the state, you'd need to subscribe to those changes. You could do this yourself, but a far better solution is to use react-redux, which is designed for connecting react components to redux stores.
For example, you'd add a provider to your app:
import { Provider } from 'react-redux';
import { store } from './Store'
export default function App () {
return (
<Provider store={store}>
<div>
<button onClick={changeName}>Click me</button>
<Name />
</div>
</Provider>
)
}
And then you'd use connect with your Name component:
import { connect } from 'react-redux';
function Name(props) {
return (
<div>
My name is: {props.name}
</div>
)
}
const mapStateToProps = (state) => {
return { name: state };
}
export default connect(mapStateToProps)(Name)

React Redux Mapping state to props not working

So I'm trying to learn React with Redux and so far I think I've been able to work out most of the code needed to make it work but I'm having an issue with getting my state passed down to my component. I am using Visual Studio 2017's ASP.NET Core project template that has react and redux boilerplate codes and they used this:
export default connect(
state => state.weatherForecasts,
dispatch => bindActionCreators(actionCreators, dispatch)
)(FetchData);
I tried doing the same thing with my own component like so:
export default connect(
state => state.lecture,
dispatch => bindActionCreators(actionCreators, dispatch)
)(LectureTable);
but when trying to access the contents of my props, the properties I want to get are tagged as undefined. I checked through Redux devtools that my initial state exists but my component is unable to see the props I'm trying to pass to it. The weird thing is I just imitated the boilerplate code but it isn't working yet the boilerplate code works just fine (ie I can go to the component and log out its initial state).
Since I'm following the format used by Visual Studio,my actioncreators, reducers, and constants are in one file shown below:
const GET_LECTURES = "GET_LECTURES";
const initialState = {
lectures: [],
selectedLecture: {},
isLoading: false,
test: 0
};
export const actionCreators = {
requestLectures: isLoading => async (dispatch) =>
{
if (!isLoading) {
// Don't issue a duplicate request (we already have or are loading the requested data)
return;
}
dispatch({ type: GET_LECTURES });
const url = `api/lecture/`;
const response = await fetch(url);
const lectures = await response.json();
dispatch({ type: RECEIVE_LECTURES, payload: lectures });
}
};
export const reducer = (state = initialState, action) => {
switch (action.type) {
case GET_LECTURES:
return { ...state, isLoading: true };
default:
return state;
}
};
I'm sorry if its all messy. I'm really just starting to begin to understand redux..
Edit
My component code:
import React, { Component } from 'react';
import {Button, Table, Label, Menu, Icon} from 'semantic-ui-react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import {actionCreators} from './../../store/Lecture';
export class LectureTable extends Component {
componentWillMount(){
// this.props.requestLectures(this.props.isLoading);
console.log(this.props.test);
}
render() {
return (
<Table size='large'>
{/*removed to make it cleaner..currently only has static data too lol*/}
</Table>
)
}
}
export default connect(
state => state.lecture,
dispatch => bindActionCreators(actionCreators, dispatch)
)(LectureTable);
where my store is configured:
import { applyMiddleware, combineReducers, compose, createStore } from 'redux';
import thunk from 'redux-thunk';
import { routerReducer, routerMiddleware } from 'react-router-redux';
import * as Lecture from './Lecture';
import * as Counter from './Counter';
import * as WeatherForecasts from './WeatherForecasts';
export default function configureStore(history, initialState) {
const reducers = {
lecture: Lecture.reducer,
counter: Counter.reducer,
weatherForecasts: WeatherForecasts.reducer
};
const middleware = [
thunk,
routerMiddleware(history)
];
// In development, use the browser's Redux dev tools extension if installed
const enhancers = [];
const isDevelopment = process.env.NODE_ENV === 'development';
if (isDevelopment && typeof window !== 'undefined' && window.devToolsExtension) {
enhancers.push(window.devToolsExtension());
}
const rootReducer = combineReducers({
...reducers,
routing: routerReducer
});
return createStore(
rootReducer,
initialState,
compose(applyMiddleware(...middleware), ...enhancers)
);
}
my index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'react-router-redux';
import { createBrowserHistory } from 'history';
import configureStore from './store/configureStore';
import App from './pages/App';
import registerServiceWorker from './registerServiceWorker';
// Create browser history to use in the Redux store
const baseUrl = document.getElementsByTagName('base')[0].getAttribute('href');
const history = createBrowserHistory({ basename: baseUrl });
// Get the application-wide store instance, prepopulating with state from the server where available.
const initialState = window.initialReduxState;
const store = configureStore(history, initialState);
const rootElement = document.getElementById('root');
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
<App />
</ConnectedRouter>
</Provider>,
rootElement);
registerServiceWorker();
The first argument to connect() should be a function that returns an object - with the props you want added as keys, and their value being the value from state. e.g.
state => ({ lecture: state.lecture })
I found the solution. First of all I'm a noob both to stackoverflow and to react so I apoligize for all my inconsistencies (if thats the right term?).
What I found out:
I am using react router
I was doing the connect method to a subcomponent of the component being rendered by the router
I placed the connect method to the parent component and it worked
Some notes:
state => state.lecture still works
I will take all of your advices to heart and change my code accordingly
The only reason I was adamant with solving the problem using the code I had was because I couldn't accept the fact that boilerplate code wouldn't work unless I had done something specifically different from what the boilerplate did. I just didn't take into account that the router played a huge role with it.
I repeat...I'm a react noob so I'm sorry for wasting your time T_T
Edit again:
I was able to connect a different child component with the Redux store. I'm trying to look at why I still can't do it for that specific component that caused me to ask this question. I'll update my answer once I find the reason.
I think in their example weatherForecasts is an object. In your example lectures seems to be an array so I suggest to rewrite your mapStateToProps function like this if you only need to get the lectures prop
state => ({ lectures: state.lectures})
if you need the whole state you can have state => state so you can access the props this.props.test and this.props.lectures
Keep in mind that mapStateToProps should return an object, not an array. By the way, in your reducer the field name is lectures (plural) not lecture so state => state.lecture will be undefined
Rick, your connect argument should be something like:
export default connect( state => {
return {
test: state.lecture // Or any value
}
})(LectureTable);
You're trying to console log the test prop, so you should include it in your connect call.
I think by doing the following steps, you can solve the issue:
First you need to call two functions when you want to connect your component to application state, one is mapDispatchToProps and another one is mapStateToProps, for your code to be clean, its better to define these functions separately and then pass them by name to connect, but if you want to use your own way you should do these changes: (assuming your reducer name is lecture from your combineReducers, and assuming you are calling requestLectures with this syntax: this.props.lectureActions.requestLectures() and importing lectureActions from the file you have written lecture related actions) :
export default connect(
state => state.lecture.lectures,
dispatch => {lectureActions: bindActionCreators(lectureActions, dispatch)}
)(LectureTable);
from above code, you do not need to export an object that contains the actions like actionCreators, you should export the requestLectures function out of it independently
add below case to your reducer so that when getting the lectures succeeds the state of the application gets updated with the lectures:
case RECEIVE_LECTURES:
return { ...state, isLoading: false, lectures: payload.lectures };
default:
return state;
}
You have two problems here.
You are defining mapStateToProps function as the first argument to connect wrong. As many of answers explain this now you should use it like, this:
export default connect(
state => ( { lecture: state.lecture } ),
dispatch => bindActionCreators(actionCreators, dispatch)
)(LectureTable);
Now, you have a lecture prop as your state. You can reach it with this.props.lecture. But in your componentWillMount method, you are trying to log it like this.props.test. It should be this.props.lecture.test.
By the way, try to use componentDidMount instead of componentWillMount since it will be deprecated in the future releases.

React Router Redux Immutable

Been using this boilerplate for a while and have been using plain JS objects as the store/state up until I started getting some odd mutations randomly so decided to switch to Immutable.js. Currently I'm trying to implement redux-immutable into my code. I'm having some issue with react-router-redux and redux-immutable; unfortunately I don't know these libraries very well 'under-the-hood' so having a lot of trouble debugging this. I have followed the instructions in the redux-immutable README on this.
Getting this error in my index.js file.
Uncaught TypeError: Cannot read property 'toJS' of undefined
index.js
const initialState = Immutable.Map({});
const store = configureStore(initialState);
const history = syncHistoryWithStore(hashHistory, store, {
selectLocationState (state) {
return state.getIn([
'route',
'location'
]).toJS();
}
});
render(
<Provider store={store}>
<Router history={history} routes={routes} />
</Provider>,
document.getElementById('root')
);
configureStore.js
const router = routerMiddleware(hashHistory);
const enhancer = compose(
applyMiddleware(thunk, router, logger),
DevTools.instrument(),
persistState(
window.location.href.match(
/[?&]debug_session=([^&]+)\b/
)
)
);
export default function configureStore(initialState) {
const store = createStore(rootReducer, initialState, enhancer);
if (module.hot) {
module.hot.accept('../reducers', () =>
store.replaceReducer(require('../reducers'))
);
}
return store;
}
rootReducer.js
import {combineReducers} from 'redux-immutable';
import routing from './Routing';
import Graphing from './Graphing';
import Syncing from './Syncing';
import Navigating from './Navigating';
import Notifying from './Notifying';
const rootReducer = combineReducers({
routing,
Graphing,
Navigating,
Syncing,
Notifying
});
export default rootReducer;
routing.js (routing reducer)
import {LOCATION_CHANGE} from 'react-router-redux';
const initialState = Immutable.fromJS({
location: {}
});
export default (state = initialState, action) => {
if (action.type === LOCATION_CHANGE) {
return state.merge({
location: action.payload
});
}
return state;
};
As per the documentation it's suggested to use routing as the reducer key. So here you are basically trying to access the store with the key routing instead of route
I just tried it without immutable, and is as below,
const history = syncHistoryWithStore(browserHistory, store, {
selectLocationState(state) {
console.log(state.routing.locationBeforeTransitions);
return state.routing;
}
});
This will return,
Hope it helps

Categories

Resources