Background:
I am practicing the idea of React/Redux. I would want to follow the flow of data.
axios dispatches action -> reducer setState to props -> Component render()
The problem may be more than 1 point. Because I am new to Frontend world.
Please feel free to re-design my app(if needed)
Problem:
company does not render out because this.props.companies is blank. But axios does fetch the array from backend.
action/index.js
//First experiment action returns promise instance
export function fetchCompanies(token) {
const jwtReady = 'JWT '.concat(token);
const headers = {
'Content-Type': 'application/json',
'Authorization': jwtReady
};
const instance = axios({
method: 'GET',
url: `${ROOT_URL}/api/companies/`,
headers: headers
});
return {
type: FETCH_COMPANIES,
payload: instance
}
}
export function getCompanies(token){
const jwtReady = 'JWT '.concat(token);
const headers = {
'Content-Type': 'application/json',
'Authorization': jwtReady
};
const instance = axios({
method: 'GET',
url: `${ROOT_URL}/api/companies/`,
headers: headers
});
return instance
.then(data=> store.dispatch('GET_COMPANIES_SUCCESS', data));
}
company_reducers.js
import {FETCH_COMPANIES, GET_COMPANIES_ERROR, GET_COMPANIES_SUCCESS} from "../actions/const";
export default function (state = {}, action) {
switch (action.type) {
case GET_COMPANIES_SUCCESS:
return {
...state,
companies: action.payload
};
case GET_COMPANIES_ERROR:
return {
...state,
err_msg: action.payload.text
};
default:
return state;
}
}
reducers/index.js
import {combineReducers} from 'redux';
import {reducer as formReducer} from 'redux-form';
import LoginReducer from './login_reducers';
import CompanyReducer from './company_reducers';
const rootReducer = combineReducers({
login: LoginReducer,
companies: CompanyReducer,
form: formReducer
});
export default rootReducer;
component/select_teams.js
import _ from 'lodash';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {fetchCompanies, getCompanies} from "../actions";
import {Link} from 'react-router-dom';
class SelectTeam extends Component {
constructor(props) {
super(props);
const token = localStorage.getItem('token');
this.state = {
token,
companies: null,
err_msg: null
}
}
componentWillMount() {
const tmp = this.props.getCompanies(this.state.token);
tmp.then(res => {
console.log(res)
})
.catch(err => {
console.log(err);
})
};
renderErrors() {
return (
<div>{this.state.err_msg}</div>
);
}
renderCompanies() {
return _.map(this.props.companies, company => {
return (
<li className="list-group-item" key={company.id}>
<Link to={`/${company.id}`}>
{company.name}
</Link>
</li>
)
});
}
render() {
if (this.props.companies === null) {
return (
<div>Loading...</div>
);
}
console.log(this.props);
return (
<div>
<h3>❤ Select Team ❤</h3>
{this.renderErrors()}
{this.renderCompanies()}
</div>
);
}
}
function mapStateToProps(state){
return {companies: state.companies}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
fetchCompanies: fetchCompanies,
getCompanies: getCompanies
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(SelectTeam);
App.js
import React, {Component} from 'react';
import './App.css';
import SelectTeam from "./components/select_teams";
import reducers from './reducers/index';
import {Provider} from 'react-redux';
import promise from "redux-promise";
import {applyMiddleware, createStore} from 'redux';
import {BrowserRouter, Route, Switch, Redirect} from 'react-router-dom';
import LoginPage from './components/loginPage';
const createStoreWithMiddleware = applyMiddleware(promise)(createStore);
const PrivateRoute = ({component: Component, isAuthorized, ...otherProps}) => (
<Route
{...otherProps}
render={props => (
isAuthorized() ? (<Component {...props} />) :
(
<Redirect to={
{
pathname: '/login',
state: {from: props.location},
}
}
/>
)
)}
/>
);
function PageNotFound() {
return (
<div>404 Page Not Found</div>
);
}
// TODO: I will add RESTful validation with backend later
function hasToken() {
const token = localStorage.getItem('token');
const isAuthenticated = !((token === undefined) | (token === null));
return isAuthenticated;
}
export const store = createStoreWithMiddleware(reducers);
class App extends Component {
//I will add security logic with last known location later.
//Get the features done first
render() {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<Switch>
<PrivateRoute exact path="/select-teams" isAuthorized={hasToken} component={SelectTeam}/>
<Route path="/login" component={LoginPage}/>
<Route component={PageNotFound}/>
</Switch>
</div>
</BrowserRouter>
</Provider>
);
}
}
export default App;
You should dispatch an action with the data fetched from the server.
Actions are pure functions that return an object (the object has at minimum a TYPE field).
If you have any async operations, you may use Redux-Thunk, which is an action creator that returns a function, and call the api fetch within it.
Here is the actions snippet:
// imports..
export const fetchCompaniesSuccess = (data) => {
retyrn {
type: FETCH_COMPANIES_SUCCESS,
data
}
}
export const fetchCompanies = (token) => dispatch => {
// ...
axios(...).then(dispatch(data => fetchCompaniesSuccess(data)))
}
In your company_reducers.js,
// Company Reducer Function, State here represents only the companies part of the store
case FETCH_COMPANIES_SUCCESS: // should match the the type returned by the action
return [
...state,
...action.data
]
// other cases & default
MAKE SURE to add redux-thunk as a middleware in your createStore, read Redux-Thunk doc for instructions.
then in you component:
componentDidMount(){
this.props.fetchCompanies(this.state.token);
}
Once companies data is added to the redux store, your component will rerender and the companies array will be available in props
You don't need to have a duplicate companies array in the component state.
You may want to Watch Dan Abramov introduction to redux, it is a free course.
Seems like your dispatch syntax is wrong. The parameter should be an object with type and payload.
return instance
.then(data=> store.dispatch({
type: 'GET_COMPANIES_SUCCESS',
payload: data
}));
Related
I am simply trying to connect() my LoginPage (component) to my Redux Store and dispatch in action via a onClick (event). When I console.log(this.props) my dispatch handler login() isn't in the component's props.
GitHub Repo -- https://github.com/jdavis-software/demo.git
Question: Why isn't my Redux Store either connection or dispatching the actions?
LoginPage:
import React, { Component} from 'react';
import { connect } from 'react-redux';
export class LoginPage extends Component<any> {
render(){
console.log('props doesnt have contain - login(): ', this.props)
return (<button onClick={ () => '' }>Login</button>)
}
}
const mapProps = state => ({ user: state.user })
const dispatchProps = (dispatch) => {
return {
login: () => dispatch({ type: 'USER_LOGGED_IN', payload: true})
}
}
export default connect(mapProps,dispatchProps)(LoginPage)
Redux Configuration:
import { IStore, IUser } from '#interfaces';
import { createStore, combineReducers } from 'redux';
import ReduxPromise from 'redux-promise';
// reducers
import userReducer from './user.reducer';
// define the intial global store state
const initialState:IStore = {
user: {
isAuthenticated: false
}
}
const appReducer = combineReducers({user: userReducer})
export default createStore(appReducer,initialState);
User Reducer:
// initial state
const initalState:IUser = {
isAuthenticated: false
}
// reducer
const userReducer = (state:IUser = initalState, { type, payload}: IPayload): IUser => {
console.log('user reducer start', state)
switch (type) {
case 'USER_LOGGED_IN':
state = { ...state, isAuthenticated: payload }
break;
default:
return state;
}
return state;
};
export default userReducer;
Root Page:
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
// styles
import './index.scss';
// pages
import { App } from '#pages';
// store
import store from './core/store/store';
render(
<Provider store={store}>
<App/>
</Provider>, document.getElementById('app')
);
I checked your code on git repository. I found out that you're exporting the named export
export class LoginPage
and the default export,
export default connect(mapProps,dispatchProps)(LoginPage)
But when you're accessing it, you're accessing it as
import { /*Other components*/ , LoginPage } from '#pages'
So it is actually taking the named exported component which is not connected to store.
I suggest you to import as
import LoginPage , { /*Other components*/ } from '#pages'
This might solve your problem.
Return statements are missing in the properties of connect.
const mapProps = state => { return {user: state.user} }
const dispatchProps = (dispatch) => {
return {
login: () => dispatch({ type: 'USER_LOGGED_IN', payload: true})
}
}
export default connect(mapProps,dispatchProps)(LoginPage)
Updated:
Please check Redux-dispatch
try:
import React, { Component} from 'react';
import { connect } from 'react-redux';
export class LoginPage extends Component<any> {
render(){
console.log('props doesnt contain - login(): ', this.props)
return (
<button onClick={ this.props.login }>Login</button>
)
}
}
const mapProps = state => ({ user: state.user })
const dispatchProps = (dispatch) => ({
login: () => dispatch({ type: 'USER_LOGGED_IN', payload: true})
})
export default connect(mapProps,dispatchProps)(LoginPage)
to return an object with Arrow Functions you need to wrap your {} with ()
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
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).
I am new to React and building a Spotify App with their API. I am using Redux Promise to resolve all promises. I can see data when I console it in my reducer of the data. But when I check my console it shows mapDispatchToProps() in Connect(App) must return a plain object. Instead received [object Promise]. I am thinking is it because I'm using Redux Promise vs thunk, but shouldn't it be able to resolve them as well?
Reducer
import { NEW_RELEASES } from '../actions/types';
export default function(state = [] , action){
console.log(action)
switch(action.type){
case NEW_RELEASES:
return [ action.payload.data, ...state ];
}
return state
}
Store
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import App from './App';
import reducers from './reducers';
import ReduxPromise from 'redux-promise'; // Look at action creator for ReduxPromise use
const createStoreWithMiddleware = applyMiddleware(ReduxPromise)(createStore);
ReactDOM.render(
<Provider store={createStoreWithMiddleware(reducers)}>
<App />
</Provider>
, document.querySelector('#root'));
Action Creator
export const getNewReleases = () => {
console.log('ran')
let request = axios.get("https://api.spotify.com/v1/browse/new-releases?country=SE", {
headers: {
'Authorization': 'Bearer ' + accessToken
}
})
return{
type: NEW_RELEASES,
payload: request
}
App.Js
import React, { Component } from 'react';
import './App.css';
import Spotify from 'spotify-web-api-js';
import { getNewReleases } from './actions'
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
const spotifyWebApi = new Spotify();
class App extends Component {
constructor(props) {
super(props)
const params = this.getHashParams();
this.state = {
welcome: "Welcome to SpotiDate",
accessToken: params.access_token,
loggedIn: params.access_Token ? true : false,
nowPlaying: {
name: 'Not Checked',
images: ''
}
}
if (params.access_token) {
spotifyWebApi.setAccessToken(params.access_token);
}
}
getHashParams() {
var hashParams = {};
var e, r = /([^&;=]+)=?([^&;]*)/g,
q = window.location.hash.substring(1);
while (e = r.exec(q)) {
hashParams[e[1]] = decodeURIComponent(e[2]);
}
return hashParams;
}
componentWillMount() {
spotifyWebApi.setAccessToken(this.state.accessToken)
localStorage.setItem('accessToken', this.state.accessToken);
const storedToken = localStorage.getItem('accessToken');
getNewReleases(storedToken);
}
render() {
return (
<div className="App">
<h3>{this.state.welcome}</h3>
<div>
<img src={this.state.nowPlaying.image} />
</div>
<button onClick={() => getNewReleases()}>Get New Releases</button>
<div className="new-releases">
</div>
</div>
);
}
}
const mapStateToProps = (state) => {
return{
newReleases: state.newReleases
}
}
const mapDispatchToProps = (dispatch) => {
return bindActionCreators(getNewReleases, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
the function bindActionCreators will take 1st argument as JSON. so it should be like below.
const mapDispatchToProps = dispatch => {
return bindActionCreators(
{
getNewReleases: getNewReleases
},
dispatch
);
};
try to use Object.assign({}, state.newReleases), or you can use the spread operator like assigning the code just like return {...state,state.newReleases}
since your object is unable to be mapped to the object state
For further understanding of this, please check this link git issue - 334
Try returning you actions in mapDispatchToProps like this:
const mapDispatchToProps = (dispatch) => ({
getNewReleases: () => dispatch(getNewReleases())
})
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.