I have gallery of images arriving from API by url. I store array with images in redux state. The problem is that props of my <Gallery> component don't get state changes.
I've read that it could be because of state mutation in reducer, but my reducer only uses concat to add images. Also I tried to use componentWillReceiveProps(), componentWillMount() for depatching actons. Nothing helped. State is updating right but this.props.images are still empty.
Other variant is that it because of asynchronous image getting, but how and why?
App.jsx
import React, {Component} from 'react';
import {createStore, compose, applyMiddleware} from 'redux';
import {Provider} from 'react-redux';
import {galleryReducer} from '../../reducers/galleryReducer';
import {Gallery} from "../Gallery/Gallery";
import './App.css';
function middleware({dispatch, getState}) {
return next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
}
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
galleryReducer,
undefined,
composeEnhancers(
applyMiddleware(middleware)
)
);
class App extends React.Component {
render() {
return (
<Provider store={store}>
<Gallery />
</Provider>
);
}
}
export default App;
Gallery.jsx
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {Picture} from "../Picture/Picture";
import {store} from "../../store";
import './Gallery.css'
class GalleryComponent extends Component {
constructor() {
super();
this.state = {
photoIndex: 0,
isOpen: false
};
this.openLightbox = this.openLightbox.bind(this);
}
async load() {
const url = 'url';
fetch(url)
.then((resp) => resp.json())
.then((data) => {
let results = data.hits;
console.log(results); //successful
store.dispatch({
type: 'APPEND_IMAGES',
images: results
});
console.log('IMAGES:', this.props.images); //empty
})
.catch(function (error) {
console.log(error);
});
}
componentDidMount() {
this.load();
}
openLightbox(index) {
this.setState({
isOpen: true,
photoIndex: index
});
}
render() {
const {photoIndex, isOpen} = this.state,
images = this.props.images;
return (
<section className="gallery">
{images.map((item, index) => <Picture openLightBox={this.openLightbox}
key={item.id}
index={index}
imgData={item}/>)}
</section>
);
}
}
const stateToProps = (state) => ({
images: state.images,
page: state.page,
error: state.error
});
export const Gallery = connect(stateToProps)(GalleryComponent);
galleryReducer.js
const initialState = {
images: [],
loading: false,
page: 1,
error: null
};
export const galleryReducer = function(state = initialState, action) {
switch (action.type) {
case 'APPEND_IMAGES':
return {
...state,
images: state.images === undefined ? action.images : state.images.concat(action.images),
loading: false
};
default:
return state
}
};
Related
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
Getting a weird error where 'map' is undefined. I'm not sure if my functions are firing at the wrong time and that's resulting in no data being received.
I'm adding Redux into my simple little application that just pulls data from an API and displays it. It's a list of a bunch of Heroes. Like I said before, I think that the error is coming from different times in the ansyc API call and when Redux is firing. But then again I'm a novice so any help is much appreciated.
import React, {useEffect} from 'react'
import { connect } from 'react-redux'
import { fetchHeroes } from '../actions/heroesActions'
import { Hero } from '../components/Hero'
const HeroesPage = ({ dispatch, loading, heroes, hasErrors }) => {
useEffect(() => {
dispatch(fetchHeroes())
}, [dispatch])
const renderHeroes = () => {
if (loading) return <p>Loading posts...</p>
if (hasErrors) return <p>Unable to display posts.</p>
return heroes.map(hero => <Hero key={hero.id} hero={hero} />)
}
return (
<section>
<h1>Heroes</h1>
{renderHeroes()}
</section>
)
}
// Map Redux state to React component props
const mapStateToProps = state => ({
loading: state.heroes.loading,
heroes: state.heroes.heroes,
hasErrors: state.heroes.hasErrors,
})
export default connect(mapStateToProps)(HeroesPage)
export const GET_HEROES = 'GET HEROES'
export const GET_HEROES_SUCCESS = 'GET_HEROES_SUCCESS'
export const GET_HEROES_FAILURE = 'GET_HEROES_FAILURE'
export const getHeroes = () => ({
type: GET_HEROES,
})
export const getHeroesSuccess = heroes => ({
type: GET_HEROES_SUCCESS,
payload: heroes,
})
export const getHeroesFailure = () => ({
type: GET_HEROES_FAILURE,
})
export function fetchHeroes() {
return async dispatch => {
dispatch(getHeroes())
try {
const response = await fetch('https://api.opendota.com/api/heroStats')
console.log(response)
const data = await response.json()
dispatch(getHeroesSuccess(data))
} catch (error) {
dispatch(getHeroesFailure())
}
}
}
index.js where I created the store
// External imports
import React from 'react'
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
import { composeWithDevTools } from 'redux-devtools-extension'
// Local imports
import App from './App'
import rootReducer from './reducers'
const store = createStore(rootReducer, composeWithDevTools(applyMiddleware(thunk)))
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
import React, {useEffect} from 'react'
import { useSelector } from 'react-redux'
import { fetchHeroes } from '../actions/heroesActions'
import { Hero } from '../components/Hero'
const HeroesPage = () => {
const data = useSelector(state => state.heroes);
useEffect(() => {
fetchHeroes();
}, [])
const renderHeroes = () => {
if (data.loading) return <p>Loading posts...</p>
if (data.hasErrors) return <p>Unable to display posts.</p>
return data.heroes.map(hero => <Hero key={hero.id} hero={hero} />)
}
return (
<section>
<h1>Heroes</h1>
{renderHeroes()}
</section>
)
}
export default HeroesPage
action file
// import store from your createStore file and access dispatch from it
dispatch = store.dispatch
export const GET_HEROES = 'GET HEROES'
export const GET_HEROES_SUCCESS = 'GET_HEROES_SUCCESS'
export const GET_HEROES_FAILURE = 'GET_HEROES_FAILURE'
export const getHeroes = () => ({
type: GET_HEROES,
})
export const getHeroesSuccess = heroes => ({
type: GET_HEROES_SUCCESS,
payload: heroes,
})
export const getHeroesFailure = () => ({
type: GET_HEROES_FAILURE,
})
export const fetchHeroes = () => {
dispatch(getHeroes())
try {
const response = await fetch('https://api.opendota.com/api/heroStats')
console.log(response)
const data = await response.json()
dispatch(getHeroesSuccess(data))
} catch (error) {
dispatch(getHeroesFailure())
}
}
reducer file
import * as actions from '../actions/heroesActions'
export const initialState = {
heroes: [],
loading: false,
hasErrors: false,
}
export default function heroesReducer(state = initialState, action) {
switch (action.type) {
case actions.GET_HEROES:
return { ...state, loading: true }
case actions.GET_HEROES_SUCCESS:
return { heroes: action.payload, loading: false, hasErrors: false }
case actions.GET_HEROES_FAILURE:
return { ...state, loading: false, hasErrors: true }
default:
return state
}
}
Please tell me why, when I call this.props.getOnEvents(), an error occurs that “getOnEvents() is not a function”, what’s wrong here and how to fix it?
I’m looking at the console.log and there is this function in the props, but when I try to call, an error flies out that it’s not a function
EventCalendar.js
import React, { Component } from 'react';
import { withStyles } from '#material-ui/core/styles';
import EventCalendarTable from './EventCalendarTable';
import EventCalendarView from './EventCalendarView';
const styles = theme => ({
root: {
width: '80%',
margin: '20px auto 0'
},
});
class EventCalendar extends Component {
constructor(props) {
super(props);
this.state = {
viewEvents: 'table'
};
}
componentDidMount() {
this.props.onGetEvents();
}
changeEventsView = (value) => {
this.setState({ viewEvents: value });
}
render() {
console.log(this.props);
const { classes } = this.props;
return (
<div className={classes.root}>
<EventCalendarView changeEventsView={this.changeEventsView}/>
{
this.state.viewEvents === 'table'
? <EventCalendarTable />
: <div>test</div>
}
</div>
);
}
}
export default withStyles(styles)(EventCalendar);
EventPage/component.jsx
import React from 'react';
import EventCalendar from '../../components/EventCalendar';
import './index.scss';
function Events(props) {
return (
<React.Fragment>
<EventCalendar props={props}/>
</React.Fragment>
);
}
export default Events;
EventPage/container.js
import { connect } from 'react-redux';
import EventsPage from './component';
import { getEvents } from '../../../store/modules/Events/operations';
const mapStateToProps = ({ Events }) => ({
Events
});
const mapDispatchToProps = dispatch => ({
onGetEvents: () => {
console.log(123);
dispatch(getEvents());
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(EventsPage);
Events/actions.js
import * as types from './types';
export const eventsFetch = value => ({
type: types.FETCHING_EVENTS,
payload: value
});
export const setEvents = ({ objById, arrayIds }) => ({
type: types.SET_EVENTS,
payload: {
eventById: objById,
eventsOrder: arrayIds
}
});
Events/types.js
export const FETCHING_EVENTS = 'Events/FETCHING_EVENTS';
export const SET_EVENTS = 'Events/SET_EVENTS';
Events/operation.js
import FetchClient from 'app/utils/FetchClient';
import IdsAndByIds from 'app/utils/IdsAndByIds';
import { eventsFetch, setEvents } from './actions';
export const getEvents = () => async (dispatch) => {
try {
const { data } = await FetchClient.get('/events');
dispatch(setEvents(IdsAndByIds(data)));
dispatch(eventsFetch(false));
} catch (error) {
console.log(error);
}
};
Events/reducer.js
import { createReducer } from 'store/utils';
import * as types from './types';
const usersInitState = {
fetching: true,
events: {
eventById: null,
usersOrder: null
},
error: null
};
const eventsReducer = createReducer(usersInitState)({
[types.FETCHING_EVENTS]: (state, { payload }) => ({
...state,
fetching: payload
}),
[types.SET_EVENTS]: (state, { payload }) => ({
...state,
events: {
...payload
}
})
});
export default eventsReducer;
Events/index.js
import { combineReducers } from 'redux';
import * as eventsListOperations from './operations';
import reducer from './reducers';
const EventsReducer = combineReducers({
eventsList: reducer
});
export default EventsReducer;
export { eventsListOperations };
The issue here is a minor one, Since you are connecting Events component to connect, you are receiveing the prop onGetEvents in that component, Now inside this component you are passing the props by a name props to the EventCalendar component
<EventCalendar props={props}/>
Now the props in EventCalender will contain a key called as props which wil lhave your data but you are trying to access it directly on props which is why it is undefined.
The correct way to pass the props here would be to use spread syntax like
<EventCalendar {...props}/>
I want to push state to the browser and append to the pathname when a subreddit has changed.
In the example below the user chooses an option from ['reactjs', 'frontend']. So when the user chooses reactjs, I want to changethe browser url to: <url>/reddit/reactjs or <url>/reddit/frontend based on the selection.
So when the user goes back and forward, I want to show data that was already fetched.
How can I make it work with react-redux for the example below? Normally, I was using history.pushState(...).
Note: I am using connected-react-router
index.js:
import 'babel-polyfill'
import React from 'react'
import { render } from 'react-dom'
import Root from './containers/Root'
render(<Root />, document.getElementById('root'))
action.js:
import fetch from 'cross-fetch'
export const REQUEST_POSTS = 'REQUEST_POSTS'
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function selectSubreddit(subreddit) {
return {
type: SELECT_SUBREDDIT,
subreddit
}
}
export function invalidateSubreddit(subreddit) {
return {
type: INVALIDATE_SUBREDDIT,
subreddit
}
}
function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}
function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}
function fetchPosts(subreddit) {
return dispatch => {
dispatch(requestPosts(subreddit))
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(subreddit, json)))
}
}
function shouldFetchPosts(state, subreddit) {
const posts = state.postsBySubreddit[subreddit]
if (!posts) {
return true
} else if (posts.isFetching) {
return false
} else {
return posts.didInvalidate
}
}
export function fetchPostsIfNeeded(subreddit) {
return (dispatch, getState) => {
if (shouldFetchPosts(getState(), subreddit)) {
return dispatch(fetchPosts(subreddit))
}
}
}
reducers.js:
import { combineReducers } from 'redux'
import {
SELECT_SUBREDDIT,
INVALIDATE_SUBREDDIT,
REQUEST_POSTS,
RECEIVE_POSTS
} from './actions'
function selectedSubreddit(state = 'reactjs', action) {
switch (action.type) {
case SELECT_SUBREDDIT:
return action.subreddit
default:
return state
}
}
function posts(
state = {
isFetching: false,
didInvalidate: false,
items: []
},
action
) {
switch (action.type) {
case INVALIDATE_SUBREDDIT:
return Object.assign({}, state, {
didInvalidate: true
})
case REQUEST_POSTS:
return Object.assign({}, state, {
isFetching: true,
didInvalidate: false
})
case RECEIVE_POSTS:
return Object.assign({}, state, {
isFetching: false,
didInvalidate: false,
items: action.posts,
lastUpdated: action.receivedAt
})
default:
return state
}
}
function postsBySubreddit(state = {}, action) {
switch (action.type) {
case INVALIDATE_SUBREDDIT:
case RECEIVE_POSTS:
case REQUEST_POSTS:
return Object.assign({}, state, {
[action.subreddit]: posts(state[action.subreddit], action)
})
default:
return state
}
}
const rootReducer = combineReducers({
postsBySubreddit,
selectedSubreddit
})
export default rootReducer
configureStore.js
import { createStore, compose, applyMiddleware } from 'redux'
import { createBrowserHistory } from 'history'
import { routerMiddleware } from 'connected-react-router'
import thunkMiddleware from 'redux-thunk'
import logger from 'redux-logger'
import rootReducer from '../reducers'
// const loggerMiddleware = createLogger()
export const history = createBrowserHistory()
export default function configureStore(preloadedState?: any) {
const store = createStore(
rootReducer(history), // root reducer with router state
preloadedState,
compose(
applyMiddleware(
thunkMiddleware,
logger,
routerMiddleware(history), // for dispatching history actions
// ... other middlewares ...
),
),
)
return store
}
Root.js
import React, { Component } from 'react'
import { Provider } from 'react-redux'
import configureStore from '../configureStore'
import AsyncApp from './AsyncApp'
const store = configureStore()
export default class Root extends Component {
render() {
return (
<Provider store={store}>
<AsyncApp />
</Provider>
)
}
}
AsnycApp.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import {
selectSubreddit,
fetchPostsIfNeeded,
invalidateSubreddit
} from '../actions'
import Picker from '../components/Picker'
import Posts from '../components/Posts'
class AsyncApp extends Component {
constructor(props) {
super(props)
this.handleChange = this.handleChange.bind(this)
this.handleRefreshClick = this.handleRefreshClick.bind(this)
}
componentDidMount() {
const { dispatch, selectedSubreddit } = this.props
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
componentDidUpdate(prevProps) {
if (this.props.selectedSubreddit !== prevProps.selectedSubreddit) {
const { dispatch, selectedSubreddit } = this.props
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
}
handleChange(nextSubreddit) {
this.props.dispatch(selectSubreddit(nextSubreddit))
this.props.dispatch(fetchPostsIfNeeded(nextSubreddit))
}
handleRefreshClick(e) {
e.preventDefault()
const { dispatch, selectedSubreddit } = this.props
dispatch(invalidateSubreddit(selectedSubreddit))
dispatch(fetchPostsIfNeeded(selectedSubreddit))
}
render() {
const { selectedSubreddit, posts, isFetching, lastUpdated } = this.props
return (
<div>
<Picker
value={selectedSubreddit}
onChange={this.handleChange}
options={['reactjs', 'frontend']}
/>
<p>
{lastUpdated && (
<span>
Last updated at {new Date(lastUpdated).toLocaleTimeString()}.{' '}
</span>
)}
{!isFetching && (
<button onClick={this.handleRefreshClick}>Refresh</button>
)}
</p>
{isFetching && posts.length === 0 && <h2>Loading...</h2>}
{!isFetching && posts.length === 0 && <h2>Empty.</h2>}
{posts.length > 0 && (
<div style={{ opacity: isFetching ? 0.5 : 1 }}>
<Posts posts={posts} />
</div>
)}
</div>
)
}
}
AsyncApp.propTypes = {
selectedSubreddit: PropTypes.string.isRequired,
posts: PropTypes.array.isRequired,
isFetching: PropTypes.bool.isRequired,
lastUpdated: PropTypes.number,
dispatch: PropTypes.func.isRequired
}
function mapStateToProps(state) {
const { selectedSubreddit, postsBySubreddit } = state
const { isFetching, lastUpdated, items: posts } = postsBySubreddit[
selectedSubreddit
] || {
isFetching: true,
items: []
}
return {
selectedSubreddit,
posts,
isFetching,
lastUpdated
}
}
export default connect(mapStateToProps)(AsyncApp)
Picker.js
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class Picker extends Component {
render() {
const { value, onChange, options } = this.props
return (
<span>
<h1>{value}</h1>
<select onChange={e => onChange(e.target.value)} value={value}>
{options.map(option => (
<option value={option} key={option}>
{option}
</option>
))}
</select>
</span>
)
}
}
Picker.propTypes = {
options: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
value: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired
}
Posts.js:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
export default class Posts extends Component {
render() {
return (
<ul>
{this.props.posts.map((post, i) => (
<li key={i}>{post.title}</li>
))}
</ul>
)
}
}
Posts.propTypes = {
posts: PropTypes.array.isRequired
}
Update:
import { push } from 'connected-react-router';
...
handleChange(nextSubreddit) {
this.props.dispatch(push('/reddit/' + nextSubreddit))
}
I placed this in the handleChange() method. When Picker changes, I push the state to the browser. However, when I go back and forward, the data does not change according to this url. I see the same data in every state.
We can handle this scenario using history property. We implement using listener of history and play with the location property which in turn provide pathname. It would be implement in componentDidUpdate. Everytime when back and forward button of browser clicked, the listener will called and service calls and state can be changed accordingly.
AsyncApp.js
// code here
import { history } from '../configureStore'
// code here
componentDidUpdate(prevProps) {
if (this.props.selectedSubreddit !== prevProps.selectedSubreddit) {
const backBrowser = history.listen(location => {
console.log(location.pathname)
// code here
}
// code here
}
}
I am building a react app and implementing redux for data. When I navigate to a particular route, I want to dispatch the action to fetch the data from an external API and then once the data comes back, display the data for the user.
Store :
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import createLogger from 'redux-logger';
import { syncHistoryWithStore } from 'react-router-redux';
import { browserHistory } from 'react-router';
import rootReducer from '../reducers/index';
const initialState = {
marvel :{
characters: []
}
};
const store = createStore(rootReducer, initialState, applyMiddleware(thunkMiddleware, createLogger()));
export const history = syncHistoryWithStore(browserHistory, store);
if (module.hot) {
module.hot.accept('../reducers/', () => {
const nextRootReducer = require('../reducers/index').default;
store.replaceReducer(nextRootReducer);
});
}
export default store;
reducer :
import * as constants from '../constants/constants';
const initialState = {
characters: [],
isFetching: false,
errorMessage: null
};
const marvelReducer = (state = initialState, action) => {
switch (action.type) {
case constants.FETCH_MARVEL :
return Object.assign({},state,{isFetching: true});
case constants.FETCH_MARVEL_SUCCESS :
return Object.assign({}. state,{
characters: [...action.response],
isFetching: false
});
case constants.FETCH_MARVEL_ERROR :
return Object.assign({}, state,{
isFetching: false,
errorMessage: action.message
});
default :
return state;
}
};
export default marvelReducer;
actions:
import 'whatwg-fetch';
import * as constants from '../constants/constants';
export const fetchMarvel = (dispatch) => {
const MARVEL_API = 'http://gateway.marvel.com:80/v1/public/characters?apikey=e542b1d89f93ed41b132eda89b9efb2c';
dispatch({
type: constants.FETCH_MARVEL
});
return fetch(MARVEL_API).then(
response => {
dispatch({
type: constants.FETCH_MARVEL_SUCCESS,
response
});
},
error => {
dispatch({
type: constants.FETCH_MARVEL_ERROR,
message: error.message || 'Something went wrong with fetchMarvel'
});
});
};
component :
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as actions from '../actions/actions';
import '../styles/homeStyles.css';
class Home extends Component {
render() {
const { characters, isFetching, errorMessage } = this.props;
return (
<div>
<h1>React Starter App</h1>
<h2>This is the home page</h2>
</div>
);
}
}
function mapStateToProps(state) {
return {
characters: state.characters,
isFetching: state.isFetching,
errorMessage: state.errorMessage
};
}
function mapDispatchToProps(dispatch) {
return { actions: bindActionCreators(actions, dispatch) };
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
I know I'm not currently displaying the props anywhere in the application, but first I am just trying to get them populated.
What step am I missing to dispatch the action so that I can populate the props from state?
You are not dispatching the action anywhere. So nothing happens.
You probably want to do this in a React lifecycle hook, for example:
class Home extends Component {
componentDidMount() {
this.props.actions.fetchMarvel();
}
render() {
const { characters, isFetching, errorMessage } = this.props;
return (
<div>
<h1>React Starter App</h1>
<h2>This is the home page</h2>
</div>
);
}
}