React: redux persistence in local storage - javascript

I'm trying out persisting data to localstorage with Redux. I just made an array of alphabet letter. Whenever I click the element with the onclick listener, I see the console log statement with a random letter each time but the UI doesn't re-render as it would with a setState() call, until I refresh the page. What am I missing here?
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import {createStore} from 'redux';
import myApp from './reducers';
import {loadState, saveState} from './localStorage'
const persistedState = loadState();
const store = createStore(
myApp,
persistedState
);
store.subscribe(() => {
saveState(store.getState());
});
function render () {
ReactDOM.render(<App store={store}/>, document.getElementById('root'));
registerServiceWorker();
}
render();
src/app.js
import React, { Component } from 'react';
import './App.css';
import { switchName } from './actions'
class App extends Component {
constructor(props) {
super(props);
this.store = this.props.store;
}
handleChangeName = () => {
let array = ["a","b","c","d","e","f","g","h","i"];
const data = array[Math.floor(Math.random() * array.length)];
this.store.dispatch(switchName(data));
}
render() {
return (
<div className="App">
<p className="ntro" onClick = {this.handleChangeName} >
Letter: {this.store.getState().name}
</p>
</div>
);
}
}
export default App;
src/localstorage.js
export const loadState = () => {
try {
const serializedState = localStorage.getItem('state');
if (serializedState === null) {
console.log("serialzed state null")
return undefined;
}
return JSON.parse(serializedState);
} catch (err) {
return undefined;
}
};
export const saveState = (state) => {
try {
console.log(state)
const serializedState = JSON.stringify(state);
localStorage.setItem('state', serializedState);
} catch (err) {};
}
src/reducers.js
const initialState = {
name: "nada"
}
export default (state = initialState, action) => {
switch (action.type) {
case "SWITCH_NAME":
return Object.assign({}, state,{
name: action.data
})
default:
return state
}
}

Your App component is not subscribing to the store and so it does not re-render when the store changes (i.e. it will only use the first name value returned from store.getState()
To fix you could use react-redux to connect to the store instead of passing it as a prop.
src/index.js
import { Provider } from 'react-redux';
// ...
function render () {
ReactDOM.render(<Provider store={store}><App store={store}/></Provider>, document.getElementById('root'));
registerServiceWorker();
}
src/app.js
import { connect } from 'react-redux'
// ...
class App extends Component {
handleChangeName = () => {
let array = ["a","b","c","d","e","f","g","h","i"];
const data = array[Math.floor(Math.random() * array.length)];
this.props.switchName(data);
}
render() {
return (
<div className="App">
<p className="ntro" onClick = {this.handleChangeName} >
Letter: {this.props.name}
</p>
</div>
);
}
}
// note that `store` is no longer used inside the `App` component
const mapState = state => ({ state.name });
const actions = { switchName };
export default connect(mapState, actions)(App);
Alternatively you can more add a render() call inside the subscribe callback in src/index.js
// ...
store.subscribe(() => {
saveState(store.getState());
render();
});
function render () {
ReactDOM.render(<App store={store}/>, document.getElementById('root'));
registerServiceWorker();
}
render();
although you might want to do something else with the registerServiceWorker() call.
Side Note: have you tried redux-persist?

Related

× TypeError: Cannot read properties of undefined (reading 'getState')

I am a beginner learning react and redux. I wrote this demo about how to use connect.js in redux. Searching this kind of question but there is no right answer for my code. I got a undefined context. Is it typo? or I passed context in a wrong way? Thanks in advance. Here is my code.
index.js
import React from "react";
import ReactDOM from "react-dom";
import store from "./store";
import { Provider } from "react-redux";
import App from "./App";
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
/store/index.js
import { createStore } from "redux";
import reducer from "./reducer.js";
const store = createStore(reducer);
export default store;
/store/reducer.js
import { ADD, SUB, MUL, DIV } from './constants.js'
// or initialState
const defaultState = {
counter: 0
}
function reducer(state = defaultState, action) {
switch (action.type) {
case ADD:
return {...state, counter: state.counter + action.num};
case SUB:
return {...state, counter: state.counter - action.num};
case MUL:
return {...state, counter: state.counter * action.num};
case DIV:
return {...state, counter: state.counter / action.num};
default:
return state;
}
}
export default reducer
connect.js
import React, { PureComponent } from "react";
import { StoreContext } from "./context";
export default function connect(mapStateToProps, mapDispatchToProps) {
return function enhanceHOC(WrappedCpn) {
class EnhanceCpn extends PureComponent {
constructor(props, context) {
super(props, context);
console.log('connect props', props);
console.log('connect context', context); // context is undefined here
this.state = {
storeState: mapStateToProps(context.getState()),
};
}
componentDidMount() {
this.unSubscribe = this.context.subscribe(() => {
this.setState({
counter: mapStateToProps(this.context.getState()),
});
});
}
componentWillUnmount() {
this.unSubscribe();
}
render() {
return (
<WrappedCpn
{...this.props}
{...mapStateToProps(this.context.getState())}
{...mapDispatchToProps(this.context.dispatch)}
/>
);
}
}
EnhanceCpn.contextType = StoreContext;
return EnhanceCpn;
};
}
context.js
import React from "react";
const StoreContext = React.createContext();
export {
StoreContext
}
App.js
import React, { PureComponent } from 'react'
import My from './pages/my'
export default class App extends PureComponent {
constructor(props, context) {
super(props, context);
console.log('APP props', props);
console.log('APP context', context); // context got value
}
render() {
return (
<div>
<My />
</div>
)
}
}
my.js
import React, { PureComponent } from 'react'
import { sub, mul } from '../store/actionCreators'
import connect from '../utils/connect'
class My extends PureComponent {
render() {
return (
<div>
<h3>my</h3>
<h3>counter: { this.props.counter }</h3>
<button onClick={e => this.props.subNum()}>-2</button>
<button onClick={e => this.props.mulNUm(5)}>*5</button>
</div>
)
}
}
const mapStateToProps = state => ({
counter: state.counter
})
const mapDispatchToProps = dispatch => ({
subNum: (num = -2) => {
dispatch(sub(num))
},
mulNUm: num => {
dispatch(mul(num))
}
})
export default connect(mapStateToProps, mapDispatchToProps)(My)
actionCreators.js
import { ADD, SUB, MUL, DIV } from './constants.js'
export function add(num) {
return {
type: ADD,
num
}
}
export const sub = (num) => {
return {
type: SUB,
num
}
}
export const mul = (num) => ({
type: MUL,
num
})
export const div = num => ({
type: DIV,
num
})
constants.js
const ADD = 'ADD_ACTION'
const SUB = 'SUB_ACTION'
const MUL = 'MUL_ACTION'
const DIV = 'DIV_ACTION'
export { ADD, SUB, MUL, DIV }
From the docs, here is what it says with regards to Class.contextType:
The contextType property on a class can be assigned a Context object
created by React.createContext(). Using this property lets you
consume the nearest current value of that Context type using
this.context. You can reference this in any of the lifecycle methods
including the render function.
It seems that in your case, you are just missing passing your custom StoreContext to redux Provider with the context props
You need to do something like:
import React from "react";
import ReactDOM from "react-dom";
import store from "./store";
import { StoreContext } from "./context";
import { Provider } from "react-redux";
import App from "./App";
ReactDOM.render(
<Provider store={store} context={StoreContext}>
<App />
</Provider>,
document.getElementById("root")
);
See also https://react-redux.js.org/using-react-redux/accessing-store#providing-custom-context
I got the same issue and solved by creating store on the same file as the root component where Provider is applied. Example code below:
<Provider store={createStore(reducers)}>
<App />
</Provider>
Cannot read properties of undefined (reading 'getState')
Solution:
(1) make a file store.js in your redux folder and you can copy the code
import { createStore, applyMiddleware } from "redux";
import logger from 'redux-logger';
import rootReducer from "./root-reducer";
const middlewares = [logger];
const store = createStore(rootReducer, applyMiddleware(...middlewares));
export default store;
(2) then just import the file in index.js file
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
i got same issues
i just have to change were i have my configure store from this
import { configureStore } from "#reduxjs/toolkit";
import basketReducer from "../slices/basketSlice";
export const store = configureStore({
reducer: {
basket: basketReducer,
},
});
to this
import { configureStore } from "#reduxjs/toolkit";
import basketReducer from "../slices/basketSlice";
export default configureStore({
reducer: {
basket: basketReducer,
},
});
and this method works for me
i also find out when i play around with the code to understand why it happen and see if i broke it what will happen this is my finding and observation from it
note concerning redux
if you used the below as import in your _app.js
import { store } from "../stores/store";
then
the global store should be rewritten like this
export const store = configureStore({
reducer: {
basket: basketReducer,
},
});
but if you import it like this with out distructuring it[store] or without the curly bracket
import store from "../stores/store";
then
you should write the store like this
export default configureStore({
reducer: {
basket: basketReducer,
},
});
Both ways works for me

Redux - Unable to use dispatch to call action inside of axios response

I am new to react and redux, this is my first attempt at using a redux action to call an API with it returning a list of products which I can then add to the redux store for me to use in any component. So far the API call is working, and returns a list of products when I add a console.log in the then response, however when I use dispatch to call the next action which sets the type I receive the error "Unhandled Rejection (TypeError): dispatch is not a function".
Here is my Fetch.js file:
import axios from "axios";
import * as React from "react";
export function loadProducts() {
return dispatch => {
return axios.get(getApiHost() + 'rest/productComposite/loadProductsWithImages/' + getId() + '/ENV_ID').then((response) => {
dispatch(getProducts(response.data.productDtos));
})
}
}
export function getProducts(productDtos) {
return {
type: 'product',
productDtos: productDtos
}
}
function getId() {
return params().get('id');
}
function getEnv() {
let env = params().get('env');
if (!env) return 'prod';
return env;
}
function getApiHost() {
}
function params() {
return new URLSearchParams(window.location.search);
}
and my reducer.js file:
const initialState = {
loaded: 'false',
productDtos: []
}
const productReducer = (state = initialState, action) => {
switch (action.type) {
case 'product':
return {
...state,
loaded: 'true',
productDtos: action.productDtos
}
default:
return {
...state,
loaded: 'false',
productDtos: action.productDtos
};
}
}
export default productReducer;
and my index.js file:
(there is lots of messy code in this file which is why i am trying to convert the project into redux)
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './components/App';
import * as serviceWorker from './serviceWorker';
import {createStore, compose, applyMiddleware } from 'redux';
import allReducers from './components/reducers'
import {Provider} from 'react-redux';
import thunk from "redux-thunk";
const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
allReducers,
composeEnhancer(applyMiddleware(thunk)),
);
ReactDOM.render(
<React.StrictMode>
<Provider store = {store}>
<App/>
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
serviceWorker.unregister();
and my app.js file:
import React from 'react';
import './App.css';
import Header from './Header';
import Footer from './Footer';
import PageHero from './PageHero';
import Products from './Products';
import ProductDetail from "./ProductDetail";
import Cart from "./Cart";
import ErrorBoundary from "./ErrorBoundary";
import PrivacyPolicy from "./PrivacyPolicy";
import LicenceAgreement from "./LicenceAgreement";
import {loadProducts} from "./Fetch";
import {connect} from "react-redux";
class App extends React.Component {
constructor(props) {
super(props);
this.cartProductsRef = React.createRef();
this.state = {
loadedShopConfig: false,
loadedLogo: false,
productView: false,
products: null,
logoUrl: null,
lAView: false,
pPView: false
};
this.onChange = this.onChange.bind(this)
this.changeBack = this.changeBack.bind(this)
this.addToCart = this.addToCart.bind(this)
this.lAViewChange = this.lAViewChange.bind(this)
this.pPViewChange = this.pPViewChange.bind(this)
}
params() {
return new URLSearchParams(window.location.search);
}
getId() {
return this.params().get('id');
}
getEnv() {
let env = this.params().get('env');
if (!env) return 'prod';
return env;
}
getApiHost() {
}
componentDidCatch(error, info) {
// Display fallback UI
this.setState({hasError: true});
// You can also log the error to an error reporting service
console.log('error', error);
console.log('info', info);
}
changeBack() {
this.setState({
productView: false,
lAView: false,
pPView: false
});
}
onChange(event) {
this.setState({productView: true});
this.setState({selectedProduct: event})
}
addToCart(product, quantity) {
console.log('cartProductsRef', this.cartProductsRef);
if (this.cartProductsRef.current) {
this.cartProductsRef.current.addToCart(product.entityInstanceId.id, quantity, product);
}
this.setState({})
}
lAViewChange() {
this.setState({lAView: true});
}
pPViewChange() {
this.setState({pPView: true});
}
loadShopDetails() {
this.setState({...this.state, isFetching: true});
fetch(this.getApiHost() + 'rest/shopComposite/' + this.getId() + '/ENV_ID')
.then((response) => response.json())
.then((responseJson) => {
console.log('Shop Details Function Results ', responseJson);
this.setState({
shopConfig: responseJson.shopConfig,
logoUrl: responseJson.logoUrl,
currencyCode: responseJson.currencyCode,
website: responseJson.website,
twitter: responseJson.twitter,
facebook: responseJson.facebook,
instagram: responseJson.instagram,
linkedIn: responseJson.linkedIn,
youTube: responseJson.youTube,
loadedShopConfig: true,
loadedLogo: true
});
})
this.setState({...this.state, isFetching: false});
}
componentDidMount() {
this.loadShopDetails();
this.props.dispatch(loadProducts());
}
render() {
let displayProduct;
const {lAView} = this.state;
const {pPView} = this.state;
const {productView} = this.state;
const {shopConfig} = this.state;
const {logoUrl} = this.state;
const {selectedProduct} = this.state;
if (productView && !lAView && !pPView) {
displayProduct = <ProductDetail
product={selectedProduct}
addToCart={this.addToCart}
/>;
} else if (!lAView && !pPView) {
displayProduct =
<Products
shopConfig={this.state.shopConfig}
productSelectedHandler={this.onChange}
/>;
}
return (
<div id="page">
<ErrorBoundary>
<p>{this.state.productView}</p>
<Header
logoUrl={this.state.logoUrl}
itemsInCart={this.state.itemsInCart}
changeBack={this.changeBack}
currencyCode={this.state.currencyCode}
/>
{!productView && !lAView && !pPView && this.state.loadedShopConfig ?
<PageHero shopConfig={this.state.shopConfig}/> : null}
{displayProduct}
<Cart id={this.getId()}
apiHost={this.getApiHost()}
ref={this.cartProductsRef}
currencyCode={this.state.currencyCode}
/>
{lAView ? <LicenceAgreement
shopConfig={this.state.shopConfig}
/> : null }
{pPView ? <PrivacyPolicy
shopConfig={this.state.shopConfig}
/> : null }
{this.state.loadedLogo ? <Footer
logoUrl={this.state.logoUrl}
lAChange={this.lAViewChange}
pPChange={this.pPViewChange}
twitter={this.state.twitter}
facebook={this.state.facebook}
instagram={this.state.instagram}
linkedIn={this.state.linkedIn}
youTube={this.state.youTube}
website={this.state.website}
/> : null}
</ErrorBoundary>
</div>
);
}
}
function mapDispatchToProps() {
return loadProducts()
}
export default connect(mapDispatchToProps)(App);
Thank you in advance for anyone who helps me, its probably a quick fix of something that I doing wrong, although I have read many articles and watched many videos and cannot find an immediate problem.
This is the way you dispatch an action in connected component. Notice that mapStateToProps is the first argument you pass into connect function, even if it returns an empty object:
import { connect } from 'react-redux'
import { loadProducts } from './path/to/actions'
class App extends React.Component {
componentDidMount() {
this.props.loadProducts();
}
render() { ... }
}
const mapStateToProps = () => ({});
const mapDispatchToProps = { loadProducts };
export default connect(mapStateToProps, mapDispatchToProps)(App);
Probably, you didn’t connect loadProducts

How do i access redux state from another react component?

I am developing a lottery statistics app that gets data from a csv loaded from an input then I was wanting to read this data to the redux store so I can use it across multiple components.
I have successfully saved the data to the redux store once I import the file and read it through Header.js and using an action, but I am not sure how to access this in other components like e.g. Main.js.
I feel like I am still confused on how react/redux all fits together. I'm sorry if this has been asked before but everything I looked up online I couldn't get to work.
// index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
import reducers from "./reducers";
import App from "./components/App";
const store = createStore(reducers, applyMiddleware(thunk));
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector("#root")
);
// App.js
import React from "react";
import Header from "./Header";
import Main from "./Main";
const App = () => {
return (
<div>
<Header />
<Main />
<div className="numbers-for-draw"></div>
</div>
);
};
export default App;
// Header.js
import React from "react";
import { CSVReader } from "react-papaparse";
import { fetchData } from "../actions";
import { connect } from "react-redux";
class Header extends React.Component {
constructor(props) {
super(props);
this.fileInput = React.createRef();
}
handleReadCSV = data => {
this.props.fetchData(data);
console.log(this.props.data);
};
handleOnError = (err, file, inputElem, reason) => {
console.log(err);
};
handleImportOffer = () => {
this.fileInput.current.click();
console.log("Got to handleImportOffer");
};
render() {
return (
<header>
<CSVReader
onFileLoaded={this.handleReadCSV}
inputRef={this.fileInput}
style={{ display: "none" }}
onError={this.handleOnError}
/>
<button onClick={this.handleImportOffer}>Import</button>
</header>
);
}
}
//Map what is in the redux store (e.g. state) to props
const mapStateToProps = state => ({
data: state.data
});
export default connect(mapStateToProps, {
fetchData: fetchData
})(Header);
// Main.js
import React from "react";
import { fetchData } from "../actions";
import { connect } from "react-redux";
const Main = () => {
console.log("In main");
console.log(this.props.data); //Blows up here.
return <div>Main</div>;
};
//Map what is in the redux store (e.g. state) to props
const mapStateToProps = state => ({
data: state.data
});
export default connect(mapStateToProps, {
fetchData: fetchData
})(Main);
// actions/index.js
export const fetchData = data => dispatch => {
console.log("Action");
const lottoData = {
stringNumbers: [
"one",
"two",
"three",
...
],
allResults: [],
winningNumbers: [],
winningNumbersAsStrings: []
};
const localData = data.data;
localData.shift();
localData.forEach(line => {
const lineObject = {
draw: line[0],
drawDate: line[1],
ballOne: line[2],
ballTwo: line[3],
ballThree: line[4],
ballFour: line[5],
ballFive: line[6],
ballSix: line[7],
bonusBall: line[8],
bonusBall2: line[9],
powerBall: line[10]
};
lottoData.allResults.push(lineObject);
let nums = [];
nums.push(parseInt(line[2]));
nums.push(parseInt(line[3]));
nums.push(parseInt(line[4]));
nums.push(parseInt(line[5]));
nums.push(parseInt(line[6]));
nums.push(parseInt(line[7]));
nums.sort((a, b) => {
if (a < b) {
return -1;
} else if (a > b) {
return 1;
} else {
return 0;
}
});
lottoData.winningNumbers.push(nums);
lottoData.winningNumbersAsStrings.push(nums.toString());
});
dispatch({ type: "FETCH_DATA", payload: lottoData });
};
// lottoReducer.js
export default (state = {}, action) => {
switch (action.type) {
case "FETCH_DATA":
return action.payload;
default:
return state;
}
};
// reducers/index.js
import { combineReducers } from "redux";
import lottoReducer from "./lottoReducer";
export default combineReducers({
data: lottoReducer
});
I haven't tested your code, but it seems to me that the only problem is in your Main.js
While you use a function component and not a class, you shouldn't use this to access your props. The following should work as expected:
const Main = (props) => {
console.log("In main");
console.log(props.data);
return <div>Main</div>;
};
//Map what is in the redux store (e.g. state) to props
const mapStateToProps = state => ({
data: state.data
});
export default connect(mapStateToProps, {
fetchData: fetchData
})(Main);
In your main.js you used functional components so this.props doesn't work there. You must pass props to your component and console.log(props.data).

Test React Component using Redux and react-testing-library

I am new to testing redux connected components in React and trying to figure out how to test them.
Currently I'm using react-testing-library and having trouble setting up the my renderWithRedux function to correctly setup redux.
Here is a sample component:
import React, { Component } from 'react'
import { connect } from 'react-redux'
class Sample extends Component {
constructor(props) {
super(props);
this.state = {
...
}
}
componentDidMount() {
//do stuff
console.log(this.props)
}
render() {
const { user } = this.props
return(
<div className="sample">
{user.name}
</div>
)
}
}
const mapStateToProps = state => ({
user: state.user
})
export default connect(mapStateToProps, {})(Sample);
Here is a sample test:
import React from 'react';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import { render, cleanup } from 'react-testing-library';
import Sample from '../components/sample/'
const user = {
id: 1,
name: "John Smith"
}}
function reducer(state = user, action) {
//dont need any actions at the moment
switch (action.type) {
default:
return state
}
}
function renderWithRedux(
ui,
{ initialState, store = createStore(reducer, initialState) } = {}
) {
return {
...render(<Provider store={store}>{ui}</Provider>),
store,
}
}
afterEach(cleanup)
test('<Sample> example text', () => {
const { getByTestId, getByLabelText } = renderWithRedux(<Sample />)
expect(getByText(user.name))
})
The user prop value always results as undefined. I have re-written this a couple of ways but can't seem to get it to work. If I pass the user data directly as a prop to Sample component in the test, it still resolves to be undefined.
I am learning from the tutorials and examples via the offical docs, like this one: https://github.com/kentcdodds/react-testing-library/blob/master/examples/tests/react-redux.js
Any pointers, tips or solutions would be greatly appreciated!
You should wrap the component inside Provider, here is the simple example
import React from 'react';
import { render } from '#testing-library/react';
import '#testing-library/jest-dom';
import { Provider } from "react-redux";
import configureMockStore from "redux-mock-store";
import TestedComponent from '../index';
const mockStore = configureMockStore();
const store = mockStore({});
const renderTestedComponent = () => {
return render(
<Provider store={store}>
<TestedComponent />
</Provider>
);
};
describe('test TestedComponent components', () => {
it('should be render the component correctly', () => {
const { container } = renderTestedComponent();
expect(container).toBeInTheDocument();
});
});
**Unable to Fire event using #testing-library**
// demo.test.js
import React from 'react'
import { Provider } from "react-redux";
import '#testing-library/react/cleanup-after-each'
import '#testing-library/jest-dom/extend-expect'
import { render, fireEvent } from '#testing-library/react'
// this is used to fire the event
// import userEvent from "#testing-library/user-event";
//import 'jest-localstorage-mock';
import ChangePassword from './ChangePassword';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
test('test 1-> Update User password', () => {
// global store
const getState = {
authUser :{
user : {
email: "test#gmail.com",
id: 0,
imageURL: null,
name: "test Solutions",
roleId: 1,
roleName: "testRole",
userName: "testUserName"
},
loading: false,
showErrorMessage: false,
errorDescription: ""
}
}; // initial state of the store
// const action = { type: 'LOGIN_USER' };
// const expectedActions = [action];
// const store = mockStore(getState, expectedActions);
const onSaveChanges = jest.fn();
const changePassword = jest.fn();
const store = mockStore(getState);
const { queryByText, getByLabelText, getByText , getByTestId , getByPlaceholderText, } = render(
<Provider store={store}>
<ChangePassword
onSaveChanges={onSaveChanges}
changePassword={changePassword}
/>
</Provider>,
)
// test 1. check the title of component
expect(getByTestId('updateTitle')).toHaveTextContent('Update your password');
// test 2. chekck the inputfile
expect(getByPlaceholderText('Old Password')) //oldpassword
expect(getByPlaceholderText('New Password')) //newpassword
expect(getByPlaceholderText('Confirm New Password')) //confpassword
// change the input values
fireEvent.change(getByPlaceholderText("Old Password"), {
target: { value: "theOldPasword" }
});
fireEvent.change(getByPlaceholderText("New Password"), {
target: { value: "#Ab123456" }
});
fireEvent.change(getByPlaceholderText("Confirm New Password"), {
target: { value: "#Ab123456" }
});
// check the changed input values
expect(getByPlaceholderText('Old Password').value).toEqual("theOldPasword");
expect(getByPlaceholderText('New Password').value).toEqual("#Ab123456");
expect(getByPlaceholderText('Confirm New Password').value).toEqual("#Ab123456");
expect(getByText('Save Changes')); // check the save change button
// calling onSave function
//fireEvent.click(getByTestId('savechange'))
// userEvent.click(getByText('Save Changes'));
})

React/Redux: State is updated in Redux object, but React component doesn't re-render

Tried to look through similar questions, but didn't find similar issues.
I am trying to implement sorts by name and amount in my app, this event is triggered in this component:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { sortByExpenseName, sortByExpenseAmount } from '../actions/expensesFilters';
class ExpensesListFilter extends Component {
onSortByExpenseName = () => {
this.props.sortByExpenseName();
};
onSortByExpenseAmount = () => {
this.props.sortByExpenseAmount();
}
render() {
return (
<div>
<span>Expense Name</span>
<button onClick={this.onSortByExpenseName}>Sort me by name</button>
<button onClick={this.onSortByExpenseAmount}>Sort me by amount</button>
</div>
)
}
}
const mapDispatchToProps = (dispatch) => ({
sortByExpenseName: () => dispatch(sortByExpenseName()),
sortByExpenseAmount: () => dispatch(sortByExpenseAmount()),
});
export default connect(null, mapDispatchToProps)(ExpensesListFilter);
for that I am using following selector:
export default (expenses, { sortBy }) => {
return expenses.sort((a, b) => {
if (sortBy === 'name') {
return a.name < b.name ? 1 : -1;
} else if (sortBy === 'amount') {
return parseInt(a.amount, 10) < parseInt(b.amount, 10) ? 1 : -1;
}
});
};
I run this selector in mapStateToProps function for my ExpensesList component here:
import React from 'react';
import { connect } from 'react-redux';
import ExpensesItem from './ExpensesItem';
// my selector
import sortExpenses from '../selectors/sortExpenses';
const ExpensesList = props => (
<div className="content-container">
{props.expenses && props.expenses.map((expense) => {
return <ExpensesItem key={expense.id} {...expense} />;
}) }
</div>
);
// Here I run my selector to sort expenses
const mapStateToProps = (state) => {
return {
expenses: sortExpenses(state.expensesData.expenses, state.expensesFilters),
};
};
export default connect(mapStateToProps)(ExpensesList);
This selector updates my filter reducer, which causes my app state to update:
import { SORT_BY_EXPENSE_NAME, SORT_BY_EXPENSE_AMOUNT } from '../actions/types';
const INITIAL_EXPENSE_FILTER_STATE = {
sortBy: 'name',
};
export default (state = INITIAL_EXPENSE_FILTER_STATE, action) => {
switch (action.type) {
case SORT_BY_EXPENSE_NAME:
return {
...state,
sortBy: 'name',
};
case SORT_BY_EXPENSE_AMOUNT:
return {
...state,
sortBy: 'amount',
};
default:
return state;
}
};
Sort event causes my state to update, the expenses array in my expenses reducer below is updated and sorted by selector, BUT the ExpensesList component doesn't re-render after my expenses array in state is updated.
What I want my ExpensesList component to do, is to re-render with sorted expenses array and sort ExpensesItem components in list.
What could be the reason why it fails? Pretty sure I am missing out something essential, but can't figure out what. My expenses reducer:
import { FETCH_EXPENSES } from '../actions/types';
const INITIAL_STATE = {};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case FETCH_EXPENSES:
return {
...state,
expenses: action.expenses.data,
};
default:
return state;
}
};
All these components are childs to this parent component:
import React from 'react';
import ExpensesListFilter from './ExpensesListFilter';
import ExpensesList from './ExpensesList';
const MainPage = () => (
<div className="box-layout">
<div className="box-layout__box">
<ExpensesListFilter />
<ExpensesList />
</div>
</div>
);
export default MainPage;
App.js file (where I run startExpenseFetch)
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import 'normalize.css/normalize.css';
import AppRouter, { history } from './routers/AppRouter';
import configureStore from './store/configureStore';
import LoadingPage from './components/LoadingPage';
import { startExpenseFetch } from './actions/expensesData';
import './styles/styles.scss';
const store = configureStore();
const jsx = (
<Provider store={store}>
<AppRouter />
</Provider>
);
let hasRendered = false;
const renderApp = () => {
if (!hasRendered) {
ReactDOM.render(jsx, document.getElementById('app'));
hasRendered = true;
}
};
store.dispatch(startExpenseFetch()).then(() => {
renderApp();
});
ReactDOM.render(<LoadingPage />, document.getElementById('app'));
Rest of files:
ExpenseItem Component:
import React from 'react';
const ExpenseItem = ({ amount, name }) => (
<div>
<span>{name}</span>
<span>{amount}</span>
</div>
);
export default ExpenseItem;
Action creators:
expensesData.js
import axios from 'axios';
import { FETCH_EXPENSE } from './types';
// no errors here
const ROOT_URL = '';
export const fetchExpenseData = expenses => ({
type: FETCH_EXPENSE,
expenses,
});
export const startExpenseFetch = () => {
return (dispatch) => {
return axios({
method: 'get',
url: `${ROOT_URL}`,
})
.then((response) => {
dispatch(fetchExpenseData(response));
console.log(response);
})
.catch((error) => {
console.log(error);
});
};
};
expensesFilters.js
import { SORT_BY_EXPENSE_NAME, SORT_BY_EXPENSE_AMOUNT } from './types';
export const sortByExpenseName = () => ({
type: SORT_BY_EXPENSE_NAME,
});
export const sortByExpenseAmount = () => ({
type: SORT_BY_EXPENSE_AMOUNT,
});
configureStores.js file
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import expensesDataReducer from '../reducers/expensesData';
import expensesFilterReducer from '../reducers/expensesFilters';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
export default () => {
const store = createStore(
combineReducers({
expensesData: expensesDataReducer,
expensesFilters: expensesFilterReducer,
}),
composeEnhancers(applyMiddleware(thunk))
);
return store;
};
AppRouter.js file
import React from 'react';
import { Router, Route, Switch, Link, NavLink } from 'react-router-dom';
import createHistory from 'history/createBrowserHistory';
import MainPage from '../components/MainPage';
import NotFoundPage from '../components/NotFoundPage';
export const history = createHistory();
const AppRouter = () => (
<Router history={history}>
<div>
<Switch>
<Route path="/" component={MainPage} exact={true} />
<Route component={NotFoundPage} />
</Switch>
</div>
</Router>
);
export default AppRouter;
Don't you have a typo on your call to your selector? :)
// Here I run my selector to sort expenses
const mapStateToProps = (state) => {
return {
expenses: sortExpenses(state.expensesData.expenses, state.expnsesFilters),
};
};
state.expnsesFilters look like it should be state.expensesFilters
Which is one of the reasons you should make your sortExpenses selector grab itself the parts of the state it needs and do it's job on its own. You could test it isolation and avoid mistakes like this.
I found a reason why it happens, in my selector I was mutating my app's state. I wasn't returning a new array from it, and was changing the old one instead, that didn't trigger my vue layer to re-render. Fixed it and it works now.

Categories

Resources