Generic Reducers/Actions in React/Redux - javascript

I am trying to determine how to pull in multiple pieces of data to use in the same component.
Every example I see with React/Redux requests very specific data and has reducers and actions to handle that exact type of data. However, I have not been able to find information about handling more generic data.
For example, I have a few different components (or categories) on my site. One of those components is Cards. So, if a user clicks on the link for /cards/hockey it should request the hockey data from the API (if it isn't in the store already), and display it in the Cards page. If a user clicks the link for /cards/football, it should follow the same procedure, checking to see if it has the data in store yet, if not pulling it from the API, and displaying the Cards page with that data.
Another component type might be stats with stats about different sports teams.
I will not always know what types of cards are available ahead of time, so I cannot hardcode the specific sports types in my application.
So in this case, I'd like to only create two components: cards and stats, but have dynamically loaded data to populate those components.
Right now I have too much repetition going on and it is hard coded. This means that I cannot dynamically add new types in the future without creating new code to handle each of these types.
So, for example, right now I have /actions/footballCardActions.js and /actions/hockeyCardActions.js. I then have /reducers/footballCardReducers.js and /reducers/hockeyCardReducers.js. I might have similar components for the Stats component as well.
I'm also specifying status such as FETCH_HOCKEY_CARDS_SUCCESS or FETCH_FOOTBALL_CARDS_SUCCESS.
Again these are all hard coded, which makes scalability difficult.
One example I am trying to follow is https://scotch.io/tutorials/bookshop-with-react-redux-ii-async-requests-with-thunks - but again it uses very specific data requests, rather than generic ones.
What can I do to make my code work more generically so that I do not need to hard code specific datasets. Are there any good tutorials out there that deal with a similar situation?
More clarification
One of my components (screens) is a sports card screen. The menu system (with links) is automatically generated on site load from an API so I do not always know what links are available. So, there may be links for hockey, football, as well as a number of other sports that I have not thought of. When the menu link is clicked, it will call the API for that sport type and display the data on the sports card screen.
Based on the above link (and other similar sites) I've figured out how to hard-code each request for a specific sport in the actions and reducers section, but I have not been able to figure out how to do this generically if I do not know the sports ahead of time.
Further clarification based on current answers
If someone adds a new sport to the API database called MuffiBall, my application needs to be able to handle it. So, I cannot be expected to add new JavaScript code for each new sport that is added to the API.
All sports cards retrieved from the database follow the same structure.
An outline of my current code
index.js
//index.js
//Other imports here (not shown)
import Cards from './components/CardsPage'
import * as cardActions from './actions/cardActions';
import * as statsActions from './actions/statsActions';
import configureStore from './store/configureStore';
const store = configureStore();
/* Bad place to put these, and currently I am expected to know what every sport is*/
store.dispatch(hockeyActions.fetchHockey());
store.dispatch(footballActions.fetchFootball());
store.dispatch(muffiballActions.fetchMuffiball());
render(
<Provider store={store}>
<Router>
<div>
/* Navigation menu here (not shown) */
/* Currently it is manually coded, */
/* but I will be automatically generating it based on API */
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/cards/:val" component={Cards} />
<Route path="/stats/:val" component={Stats} />
</div>
</Router>
</Provider>,
document.getElementById('app')
);
store/configureStore.js
// store/configureStore.js
import {createStore, compose, applyMiddleware} from 'redux';
// Import thunk middleware
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
return createStore(rootReducer, initialState,
// Apply to store
applyMiddleware(thunk)
);
}
actions/actionTypes
// actions/actionTypes
export const FETCH_HOCKEY_SUCCESS = 'FETCH_HOCKEY_SUCCESS';
export const FETCH_FOOTBALL_SUCCESS = 'FETCH_FOOTBALL_SUCCESS';
export const FETCH_MUFFIBALL_SUCCESS = 'FETCH_MUFFIBALL_SUCCESS';
actions/hockeyActions.js (one such file for every sport - need to make this one generic file):
// hockeyActions.js (one such file for every sport - need to make this one generic file):
import Axios from 'axios';
const apiUrl = '/api/hockey/';
// Sync Action
export const fetchHockeySuccess = (hockey) => {
return {
type: 'FETCH_HOCKEY_SUCCESS',
hockey
}
};
//Async Action
export const fetchHockey = () => {
// Returns a dispatcher function
// that dispatches an action at a later time
return (dispatch) => {
// Returns a promise
return Axios.get(apiUrl)
.then(response => {
// Dispatch another action
// to consume data
dispatch(fetchHockeySuccess(response.data))
})
.catch(error => {
console.log(error)
throw(error);
});
};
};
reducers/hockeyReducers.js (one such file for every sport - need to make this one generic file)
// reducers/hockeyReducers.js (one such file for every sport - need to make this one generic file)
import * as actionTypes from '../actions/actionTypes'
export const hockeyReducer = (state = [], action) => {
switch (action.type) {
case actionTypes.FETCH_HOCKEY_SUCCESS:
return action.hockey;
default:
return state;
}
};
reducers/index.js
// reducers/index.js
import { combineReducers } from 'redux';
import {hockeyReducer} from './hockeyReducers'
import {footballReducer} from './footballReducers'
import {muffiballReducer} from './muffiballReducers'
export default combineReducers({
hockey: hockeyReducer,
football: footballReducer,
muffiball: muffiballReducer,
// More reducers for each sport here
});
components/CardsPage.js:
//components/CardsPage.js
import React from 'react';
import { connect } from 'react-redux';
class Cards extends React.Component{
constructor(props){
super(props);
this.state = {
data: this.props.data,
}
}
componentWillReceiveProps(nextProps){
this.setState({
data: nextProps.data,
})
}
render(){
return(
{/* cards displayed from this.state.data */}
)
}
}
const mapStateToProps = (state, ownProps) => {
return {
data: state[ownProps.match.params.val]
}
};
export default connect(mapStateToProps)(Cards);

take a step back and identify the data types that have unique shapes, eg cards and stats. You will build a store slice for each of these with it's own actions, reducers, and selectors. The sport should just be a variable you use as an argument to your actions and selectors.
eg
Async Action
export const fetchCards = (sport) => {
return (dispatch) => {
return Axios.get(`/api/${sport}/`)
.then(response =>
dispatch(fetchCardSuccess({ sport, data: response.data }))
)
.catch(error => {
console.log(error)
throw(error);
});
};
};
Reducer
export const cardReducer = (state = {}, action) => {
switch (action.type) {
case actionTypes.FETCH_CARD_SUCCESS:
return { ...state, [action.sport]: action.data };
default:
return state;
}
};
Card Selector
export const getSport(state, sport) {
return state.cards[sport];
}
You'll probably want another slice for managing a list of the available sports, fetched from the server, and other global data.

Soo this assumes your "generic data" always will have the same shape.
You could have a generic <Results /> component. Not sure how you are doing routing, but you can use the path name of the URL to determine which data to fetch and display.
The route component (React Router 4) could look like this:
<Route path="/cards/:id" render={props => <Results {...props} />}
Then in your <Results/> component you can use react-redux to map your redux state to the component props. In componentDidMount you could see if you have the appropriate data. If you do not have the appropriate data then dispatch an action from componentDidMount to fetch it. Something like this
import { connect } from 'react-redux';
import React from 'react';
import { fetchDataAction } from './actions';
class Results extends React.Component {
componentDidMount() {
// check if results exists, if not then fire off an action to get
// data. Use whatever async redux pattern you want
if (!this.props.results) {
this.props.fetchData();
}
}
render() { /* DO SOMETHING WITH RESULTS, OR LACK OF */ }
}
const mapStateToProps = (state, ownProps) => ({
results: state.results[ownProps.match.params.id],
});
const mapDispatchToProps = (dispatch, ownProps) => ({
fetchData() {
// send path parameter via action to kick off async fetch
dispatch(fetchDataAction(ownProps.match.params.id));
},
});
export default connect(mapStateToProps, mapDispatchToProps)(Results);
You could have a results reducer that would just be an object that maps category to results. Here is what the results reducer could look like:
export default (state = {}, action) => {
switch(action.type) {
case 'FETCH_LOADED':
const { payload: { type, results } } = action;
return {
...state,
[type]: results,
};
default:
return state;
};
};

A methodology that is picking up popularity for reusable redux actions/reducers is Redux Ducks. Here's a good helper library and example to implement this in your codebase.
Building off the example in the above link that would look something like this for you:
// remoteObjDuck.js
import Duck from 'extensible-duck'
import axios from 'axios'
export default function createDuck({ namespace, store, path, initialState={} }) {
return new Duck({
namespace, store,
consts: { statuses: [ 'NEW', 'LOADING', 'READY', 'SAVING', 'SAVED' ] },
types: [
'UPDATE',
'FETCH', 'FETCH_PENDING', 'FETCH_FULFILLED',
'POST', 'POST_PENDING', 'POST_FULFILLED',
],
reducer: (state, action, { types, statuses, initialState }) => {
switch(action.type) {
case types.UPDATE:
return { ...state, obj: { ...state.obj, ...action.payload } }
case types.FETCH_PENDING:
return { ...state, status: statuses.LOADING }
case types.FETCH_FULFILLED:
return { ...state, obj: action.payload.data, status: statuses.READY }
case types.POST_PENDING:
case types.PATCH_PENDING:
return { ...state, status: statuses.SAVING }
case types.POST_FULFILLED:
case types.PATCH_FULFILLED:
return { ...state, status: statuses.SAVED }
default:
return state
}
},
creators: ({ types }) => ({
update: (fields) => ({ type: types.UPDATE, payload: fields }),
get: (id) => ({ type: types.FETCH, payload: axios.get(`${path}/${id}`),
post: () => ({ type: types.POST, payload: axios.post(path, obj) }),
patch: () => ({ type: types.PATCH, payload: axios.patch(`${path}/${id}`, obj) })
}),
initialState: ({ statuses }) => ({ obj: initialState || {}, status: statuses.NEW, entities: [] })
})
}
and each sport would create a single duck that will reuse the same functionality.
Hockey:
// hockeyDuck.js
import createDuck from './remoteObjDuck'
export default createDuck({ namespace: 'my-app', store: 'hockeyCards', path: '/cards/hockey' })
Football:
// footballDuck.js
import createDuck from './remoteObjDuck'
export default createDuck({ namespace: 'my-app', store: 'footballCards', path: '/cards/football' })
Then combine the reducers in the store:
// reducers.js
import { combineReducers } from 'redux'
import footballDuck from './footballDuck'
import hockeyDuck from './hockeyDuck'
export default combineReducers({ [footballDuck.store]: footballDuck.reducer, [hockeyDuck.store]: hockeyDuck.reducer })
If you want to dynamically add reducers to redux on the fly you will have to use something like: https://github.com/ioof-holdings/redux-dynamic-reducer. Then you can create the duck on the fly depending on your API call response:
//get from API
var sport = "football";
var footballDuck = createDuck({ namespace: 'my-app', store: 'cards', path: `/cards/${sport}` });
store.attachReducer({ [footballDuck.store]: footballDuck.reducer });

// structure (something like...)
/*
./components
./redux
./redux/actions
./redux/reducers
./redux/sagas
./redux/types
./util
*/
/* ------------------------------------------------- */
/* package.json */
{
(...)
"proxy": "http://localhost:3000",
(...)
}
/* ------------------------------------------------- */
/* index.js or otherComponent.js */
import React from 'react'
import { render } from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux'
import reducers from './redux/reducers/index'
import logger from 'redux-logger'
import createSagaMiddleware from 'redux-saga'
import indexSagas from './redux/sagas/indexSagas'
import { environment } from './util/baseUrl'
const sagaMiddleware = createSagaMiddleware()
const store =
environment === 'DEV' ?
createStore(
reducers,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__(),
applyMiddleware(sagaMiddleware, logger)
) :
createStore(
reducers,
applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(indexSagas)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app'))
/* ------------------------------------------------- */
/* baseURL.js */
const DEV = 'DEV'
const PROD = 'PROD'
/*-----------------------------------------*/
/*------*/ export const environment = DEV /* <------- */
/*-----------------------------------------*/
export const baseURL =
environment === DEV ?
'/api/v1/' :
'https://abcde.website.net/api/v1/'
/* ------------------------------------------------- */
/* genericTypes.js */
export const GET_REGISTERS_REQUEST = 'GET_REGISTERS_REQUEST'
export const GET_REGISTERS_SUCCESS = 'GET_REGISTERS_SUCCESS'
export const GENERIC_ERROR_MSG = 'GENERIC_ERROR_MSG'
/* ------------------------------------------------- */
/* actions.js */
export const getRegistersRequest = ( route ) => {
return {
type: GET_REGISTERS_REQUEST,
route,
}
}
export const getRegistersSuccess = ( data ) => {
return {
type: GET_REGISTERS_SUCCESS,
data,
}
}
export const genericErrorMsg = ( errorMsg ) => {
return {
type: GENERIC_ERROR_MSG,
errorMsg,
}
}
/* ------------------------------------------------- */
/* genericReducer.js */
import { GET_REGISTERS_REQUEST, GET_REGISTERS_SUCCESS, GENERIC_ERROR_MSG } from '../types/genericTypes'
const INITIAL_STATE = {
data: [],
isFetching: false,
isLoaded: false,
error: false,
errorMsg: '',
}
const genericReducer = (state = INITIAL_STATE, action) => {
switch(action.type){
case GET_REGISTERS_REQUEST:
return {
...state,
data: [],
isFetching: true,
isLoaded: false,
error: false,
errorMsg: '',
}
case GET_REGISTERS_SUCCESS:
return {
...state,
data: action.data,
isFetching: false,
isLoaded: true,
}
case GENERIC_ERROR_MSG:
return {
...state,
isFetching: false,
error: true,
errorMsg: action.errorMsg,
}
default:
return state
}
}
export default genericReducer
/* ------------------------------------------------- */
/* yourComponent.js */
import React, { Component } from "react"
import { connect } from 'react-redux'
import { getRegistersRequest } from '../../redux/actions'
//(...)
// this.props.getRegistersRequest('cards/hockey')
// this.props.getRegistersRequest('cards/football')
//(...)
const mapStateToProps = (state) => {
return {
data: state.genericReducer.data,
isFetching: state.genericReducer.isFetching,
isLoaded: state.genericReducer.isLoaded,
error: state.genericReducer.error,
errorMsg: state.genericReducer.errorMsg,
}
}
const mapDispatchToProps = (dispatch) => {
return {
getRegistersRequest: ( route ) => dispatch(getRegistersRequest( route )),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(yourComponent)
/* ------------------------------------------------- */
/* indexSagas.js */
import { takeLatest } from 'redux-saga/effects'
import axios from 'axios'
import { GET_REGISTERS_REQUEST } from '../types/genericTypes'
import { getRegistersRequest } from './genericSagas'
function* indexSagas() {
try {
yield (takeLatest(GET_REGISTERS_REQUEST, getRegistersRequest, axios))
}
catch (e) {
// (...)
}
}
export default indexSagas
/* ------------------------------------------------- */
/* genericSagas.js */
import { put } from 'redux-saga/effects'
import { getRegistersSuccess, genericErrorMsg } from '../actions'
export function* getRegistrosRequest(axios, action) {
const rest = createRest(axios)
try {
let route = ''
switch (action.route) {
case 'cards/hockey':
case 'cards/football':
route = action.route
break
default: {
yield put(genericErrorMsg('Route [ ' + action.route + ' ] not implemented yet!'))
return
}
}
const data = yield rest.get(route)
yield put(getRegistersSuccess(data))
}
catch (e) {
yield put(genericErrorMsg(e))
}
}
/* ------------------------------------------------- */
/* createRest */
import { baseURL } from '../../util/baseUrl'
function createRest(axios){
const token = localStorage.getItem('yourToken')
const rest = axios.create({
baseURL: baseURL,
headers:{
Authorization: 'Bearer ' + token
}
})
return rest
}
export default createRest
/* ------------------------------------------------- */

Related

Redux store property gets nested into itself on update

I'm having this issue where my props are ending up looking like this (on console.log):
{
fetchRoles: f(),
roles:
roles: ["Admin", "Manager"],
}
As you can see, somewhere in my code I'm making a mistake that causes the roles prop to get nested into itself, which would force me into doing const { roles } = this.props.roles; in order to retrieve my data (which works BTW).
I've looked around for help but not many people seem to have run into this issue (I'm just getting started with redux).
Below you can see my files:
rolesReducer.js:
import { FETCH_ROLES } from "../actions/types";
const initialState = {
roles: [],
};
export default function (state = initialState, action) {
const { roles } = action;
switch (action.type) {
case FETCH_ROLES:
return {
...state, //also tried ...state.roles and same issue.
roles,
};
default:
return state;
}
}
rolesActions.js:
import { FETCH_ROLES } from "./types";
const roles = ["SuperAdmin"];
export function fetchRoles() {
return function (dispatch) {
dispatch({
type: FETCH_ROLES,
roles,
});
};
}
reducers/index.js (root reducer):
import { combineReducers } from "redux";
import rolesReducer from "./rolesReducer";
import roleMembersReducer from "./roleMembersReducer";
export default combineReducers({
roles: rolesReducer,
roleMembers: roleMembersReducer,
});
PermissionsManager.jsx:
import React, { Component } from "react";
import { connect } from "react-redux";
import { Container } from "react-bootstrap";
import { fetchRoles } from "../redux/actions/rolesActions";
import RoleContainer from "./RoleContainer";
class PermissionsManager extends Component {
componentDidMount() {
this.props.fetchRoles();
}
render() {
console.log(this.props);
const { roles } = this.props.roles;
return (
<Container>
{roles.map((role) => {
return <RoleContainer key={role} role={role} />;
})}
</Container>
);
}
}
const mapStateToProps = (state) => {
return {
roles: state.roles,
};
};
export default connect(mapStateToProps, { fetchRoles })(PermissionsManager);
Edit 1 - Adding reducer log:
As suggested, I logged the reducer, specifically state and action:
state:
{
roles: [],
}
action:
{
roles: ["Admin", "Manager"],
type: "FETCH_ROLES",
}
No duplication or abnormal structures I believe.
One way to shape the store like you're asking is to flatten roles in rolesReducer.js,
you can do so storing the received array directly in the partial state:
initialState would need to look like
const initialState = []
and in the switch statement
case FETCH_ROLES:
return roles

combineReducers is overwriting state instead of setting up different states

I'm trying to learn Redux.
I have this action which fetches data:
export const FETCH_SUCCESS = "FETCH_SUCCESS";
import axios from "axios";
export const fetchData = () => {
return async dispatch => {
try {
const result = await axios(
`url`
);
dispatch({ type: FETCH_SUCCESS, payload: { result } });
} catch (error) {
console.log(error);
}
};
};
And this reducer:
import { FETCH_SUCCESS } from "../actions/mainCategories";
const initialState = {
mainCategories: []
};
const mainCategoriesReducer = (state = initialState, action) => {
switch (action.type) {
case FETCH_SUCCESS:
return { data: action.payload.result.data };
default:
return state;
}
};
export default mainCategoriesReducer;
I use this twice, one for the mainCategories and the exact same code for my subcategories but in different files, so that I can combine it in App.js like this:
import mainCategoriesReducer from "./store/reducers/mainCategories";
import subCategoriesReducer from "./store/reducers/subCategories";
const rootReducer = combineReducers({
mainCategories: mainCategoriesReducer,
subCategories: subCategoriesReducer,
});
const store = createStore(rootReducer, applyMiddleware(ReduxThunk));
I wrap the Provider with the stored store around my App:
<Provider store={store}>
<App />
</Provider>
In the components I use it like this:
import { fetchData } from "../store/actions/mainCategories";
const mainCategories = useSelector(state => state.mainCategories);
useEffect(() => {
dispatch(fetchData());
}, [dispatch]);
And the same for the subCategories but with the corresponding imports and states (state.subCategories)
Everything is working as expected. When the App loads, it fetches my mainCategories. I can navigate to my subcategories but when I go back, the mainCategories are overwritten by the subCategories.
It seems like the combineReducers merges / overwrites instead of creating the different states. What am I doing wrong? Thank you
You need different type names to make this work. When you combine reducers, it's important that the app knows how to route your actions to the correct store. If you want to call them both FETCH_SUCCESS, I think that's fine, but add the path in front of the definition:
in your main categories actions file:
export const FETCH_SUCCESS = "mainCategories/FETCH_SUCCESS";
and in your subcategories actions file:
export const FETCH_SUCCESS = "subCategories/FETCH_SUCCESS";
That should work, but you may also need to rename the FETCH_SUCCESS variables if it's still not working.

React Native: TypeError: undefined is not an object (evaluating '_this.props.data.map')

I wonder if React Native has a bug that needs fixing that gives the following error:
React Native: TypeError: undefined is not an object (evaluating
'_this.props.data.map')
I am pretty good at this and yet I cannot seem to resolve why I am getting this error when I put together this component:
import React, { Component } from "react";
import { View, Animated } from "react-native";
class Swipe extends Component {
renderCards() {
return this.props.data.map(item => {
return this.props.renderCard(item);
});
}
render() {
return <View>{this.renderCards()}</View>;
}
}
export default Swipe;
I have checked and double checked through various debugging practices that the problem is not with my action creator or reducer and after various refactors I got those working correctly.
I decided to do the above component from scratch whereas before I was reusing another component and yet I still get the above error.
I ask if it's a bug with RN because someone else posted a similar problem but they did not get the answer they needed.
It is not a scope issue with this because if I refactor it like so:
renderCards = () => {
return this.props.data.map(item => {
return this.props.renderCard(item);
});
};
It does absolutely nothing for me, same error message. The message saying is not an object is confusing too, it's an array and map() can only iterate through arrays, so not sure what not being an object has to do with it.
The above component is being called in this screen:
import React, { Component } from "react";
import { View, Text } from "react-native";
import { connect } from "react-redux";
import Swipe from "../components/Swipe";
class DeckScreen extends Component {
renderCard(job) {
return (
<Card title={job.title}>
<View style={styles.detailWrapper}>
<Text>{job.company}</Text>
<Text>{job.post_date}</Text>
</View>
<Text>
{job.description.replace(/<span>/g, "").replace(/<\/span>/g, "")}
</Text>
</Card>
);
}
render() {
return (
<View>
<Swipe data={this.props.jobs} renderCard={this.renderCard} />
</View>
);
}
}
const styles = {
detailWrapper: {
flexDirection: "row",
justifyContent: "space-around",
marginBottom: 10
}
};
function mapStateToProps({ jobs }) {
return { jobs: jobs.listing };
}
export default connect(mapStateToProps)(DeckScreen);
This is what the action creator looks like:
import axios from "axios";
// import { Location } from "expo";
import qs from "qs";
import { FETCH_JOBS, LIKE_JOB } from "./types";
// import locationify from "../tools/locationify";
const JOB_ROOT_URL = "https://authenticjobs.com/api/?";
const JOB_QUERY_PARAMS = {
api_key: "5634cc46389d0d872723b8c46fba672c",
method: "aj.jobs.search",
perpage: "10",
format: "json"
};
const buildJobsUrl = () => {
const query = qs.stringify({ ...JOB_QUERY_PARAMS });
return `${JOB_ROOT_URL}${query}`;
};
export const fetchJobs = (region, callback) => async dispatch => {
try {
const url = buildJobsUrl();
let { data } = await axios.get(url);
dispatch({ type: FETCH_JOBS, payload: data });
callback();
} catch (e) {
console.log(e);
}
};
export const likeJob = job => {
return {
payload: job,
type: LIKE_JOB
};
};
and reducer:
import { FETCH_JOBS } from "../actions/types";
const INITIAL_STATE = {
listing: []
};
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_JOBS:
return action.payload;
default:
return state;
}
}
and the combineReducer is setup correctly as well:
import { combineReducers } from "redux";
import auth from "./auth_reducer";
import jobs from "./jobs_reducer";
import likedJobs from "./likes_reducer";
export default combineReducers({
auth,
jobs,
likedJobs
});
The listing: [] is based off the structure of the response I get back. When I console.log(data);, the actual data I care about is inside of listing property. So I set up the INITIAL_STATE to default listing to be an empty array with the intent to ensure I could map over the array and not worry about the case where I have not yet fetched the list of jobs. When I go to the API endpoint directly you can see it below:
I think the problem is simply that this.props.jobs is undefined. Your initial state is defined as { listing: [] }, however you mapStateToProps do { jobs: ... }.
Try changing initialState to { jobs: [] }, so that it always work on your first rendering.
I think your mapStateToProps should be:
mapStateToProps = (state) => {
return { jobs: listings.listing }
}
EDIT
Actually, it could be even better if you 'name' your state correctly in your reducer, like:
const INITIAL_STATE = { jobs: [] }
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_JOBS:
const jobs = action.payload.listings.listing
return { ...state, jobs };
default:
return state;
}
}
Then in your mapStateToProps:
mapStateToProps = ({ jobs }) => {
return { jobs }
}
The issue is in your reducer. Please refer the below changes:
import { FETCH_JOBS } from "../actions/types";
const INITIAL_STATE = {
listing: []
};
export default function(state = INITIAL_STATE, action) {
switch (action.type) {
case FETCH_JOBS:
const { listings } = action.payload
return {...state, listing: listings.listing}
default:
return state;
}
}
Hope this will help.
function mapStateToProps({ jobs }) {
return { jobs: jobs.listing };
}
the above is making confusion for you try the below one
try to put
function mapStateToProps( state ) {
return { jobs: state.jobs.listing };
}
as you have defined your reducer as follow
export default combineReducers({
auth,
jobs,
likedJobs
});
jobs is your variable to access jobs reducer

ReactJS: Redux state is not changing

I'm just starting with React and Redux and stumbled upon something I can't figure out by myself - I think that Redux state is not changing and it's causing (some of) errors. I'm checking state with use of remote-redux-devtools#0.5.0.
My code:
Categories.js:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import { getCategories } from '../../actions/categories';
export class Categories extends Component {
static propTypes = {
categories: PropTypes.array.isRequired
};
componentDidMount() {
this.props.getCategories();
}
render() {
return (
<div>
Placeholder for categories.
</div>
)
}
}
const mapStateToProps = state => ({
categories: state.categories.categories
});
export default connect(mapStateToProps, { getCategories })(Categories);
../../actions/categories.js:
import axios from "axios";
import { CATEGORIES_GET } from "./types";
export const getCategories = () => dispatch => {
return axios
.get("/api/notes/categories/")
.then(res => {
dispatch({
type: CATEGORIES_GET,
payload: res.data
});
})
.catch(err => console.log(err));
};
reducers/categories.js:
import { CATEGORIES_GET } from '../actions/types.js';
const initialState = {
categories: []
};
export default function (state = initialState, action) {
switch (action.type) {
case CATEGORIES_GET:
return {
...state,
categories: action.payload
};
default:
return state;
}
}
store.js:
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'remote-redux-devtools';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const initialState = {};
const middleware = [thunk];
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware)));
export default store;
reducers/index.js
import { combineReducers } from "redux";
import categories from './categories';
export default combineReducers({
categories,
});
Using remote-redux-devtools, I've never seen anything in my state. Currently this code above gives me 3 errors, two of them being
this.props.getCategories is not a function
My guess is that because there is some issue with Categories class, it's not passing anything to state and it could be root cause of errors. I had one more error, connected to Categories not being called with attributes, but for debug purposes I put empty array there - one error dissapeared, but that's it. I've also tried adding constructor to Categories and called super(), but did not help also.
I believe your issue is that you're exporting your Categories class twice, once connected, the other not.
If you remove export from export class Categories extends Component, does it work as expected?
When you're mapping the state in a component, you must access the desired variable through a reducer.
So instead of:
const mapStateToProps = state => ({
categories: state.categories
});
You must use:
const mapStateToProps = state => ({
categories: state.categories.categories
});
Your props don't have getCategories method, because you didn't pass it as a function to connect.
A better approach is to define only the action code in your actions file and then use mapDispatchToProps.
../../actions/categories.js
import axios from "axios";
export const getCategories = () => {
axios
.get("/api/notes/categories/")
.then(res => res.data);
})
.catch(err => console.log(err));
};
Categories.js
import { getCategories } from '../../actions/categories'
import { CATEGORIES_GET } from "./types";
const mapDispatchToProps = dispatch => {
return {
getCategories: () => dispatch(() => { type: CATEGORIES_GET, payload: getCategories() }),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Categories);

Re render Component when props updates (w/ Redux)

Ok so i have 2 components, Map and App. App component basically holds the map.
This is the code of my App:
import React, { Component } from 'react';
import { Text, View } from 'react-native';
import { StackNavigator, TabNavigator } from 'react-navigation';
//Redux
import { Provider } from 'react-redux';
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
//Screen Imports
import CenterNew from './screens/CenterNew';
import Map from './components/Map';
import Profile from './containers/Profile';
import Review from './containers/Review';
import { connect } from 'react-redux';
var store = createStore(rootReducer, {}, applyMiddleware(thunk));
class App extends Component {
render () {
{ console.ignoredYellowBox = ['Remote debugger']; }
const MainScreenNavigator = TabNavigator({
Centers: {screen: Map},
Review: {screen: Review},
Profile: {screen: Profile}
});
MainScreenNavigator.navigationOptions = {
title: this.props.map.title
};
const Screens = StackNavigator({
Home: {screen: MainScreenNavigator},
CenterNew: {screen: CenterNew}
});
return (
<Provider store={store}>
<Screens />
</Provider>
);
}
}
const connectWithStore = (store, WrappedComponent, mapStateToProps) => {
let ConnectedWrappedComponent = connect(mapStateToProps)(WrappedComponent);
return (props) => {
return <ConnectedWrappedComponent {...props} store={store} />
}
}
const mapStateToProps = (state) => {
return {
map: state.map
};
};
App = connectWithStore(createStore(rootReducer, {}, applyMiddleware(thunk)), App, mapStateToProps);
export default App;
In the snippet, i set the title using
MainScreenNavigator.navigationOptions = {
title: this.props.map.title
};
So whenever App is rendered, I log the initial state as you could see in componentWillMount() and this is the result. A syou can see, the initial title is 'Home'
now, in my Map I have a button which triggers this action creator just to change the header text:
this.props.changeHeaderTitle('Test');
console.log(this.props.map);
code:
export const changeHeaderTitle = (title) => {
return {
type: 'CHANGE_HEADER_TITLE',
payload: title
};
};
and this is my reducer:
const INITIAL_STATE = {
isPinnable: false,
pinnedCoordinateLatitude: 0,
pinnedCoordinateLongitude: 0,
region: {
latitude: 14.582524,
longitude: 121.061547,
latitudeDelta: 0.007,
longitudeDelta: 0.007
},
title: '',
searched: false
};
export default (state = INITIAL_STATE, action) => {
switch(action.type) {
case 'ENABLE_PINNING':
return { ... state, isPinnable: true }
case 'DISABLE_PINNING':
return { ... state, isPinnable: false,
pinnedCoordinateLatitude: action.payload.latitude,
pinnedCoordinateLongitude: action.payload.longitude }
case 'GET_PINNED_COORDINATE':
let test = action.payload.longitude;
return {
...state,
isPinnable: false,
pinnedCoordinateLatitude: action.payload.latitude,
pinnedCoordinateLongitude: action.payload.longitude
}
case 'GET_CURRENT_REGION':
return { ...state, region: region }
case 'CHANGE_CURRENT_REGION':
return { ...state, region: action.payload}
case 'CHANGE_HEADER_TITLE':
return { ...state, title: action.payload }
case 'SEARCHED_VIEW':
return { ...state, searched: action.payload }
default:
return state;
}
};
whenever the action is triggered, i know the title is updated because i log it as you can see in
this.props.changeHeaderTitle('Test');
console.log(this.props.map);
but in my view, the title is still 'Home'.. I referred to this: rerender react component when prop changes and tried to use componentWillRecieveProps() but it is not logging when this.props.changeHeaderTitle('Test'); action is triggered from Map component.
You need to connect your Redux store with your component. Therefore you should add this snippet:
mapStateToProps(store){
const { title } = store;
return { title };
}
export default connect(mapStateToProps, { changeHeaderTitle })(App)
The mapStateToProps function will subscribe to redux store updates. Every time your state changes, your component will rerender automatically. You can find more information about connecting components to the redux store here: react-redux api docs
You can't update the screen title like in the code you provide.
//This is only needed if you want to set the title from another screen
static navigationOptions = ({ navigation }) => {
const {state} = navigation;
return { title: `${state.params.title}`,
};
};
ChangeThisTitle = (titleText) => {
const {setParams} = this.props.navigation; setParams({ title: titleText })
}
//Your render method
render(){
//Views goes here
}
To change the title you have call the change title method from the onPress.
You may please refer this link for more details.

Categories

Resources