How to display data from redux store? - javascript

I have created action and reducer for saving messages (array) in redux store. I have created actions and reducer for it but how can I display data once it is stored in redux store ?
reducer.js:
import { SAVE_ITEMS, SAVE_MESSAGES} from '../actions/types';
const initialState = {
messages: [],
items: []
}
export default function (state = initialState, action) {
switch (action.type) {
case SAVE_MESSAGES:
return {
...state,
messages: action.payload
};
default:
return state;
}
}
action.js:
import { SAVE_MESSAGES } from './types';
export const saveMessages = (messages) => ({
type: SAVE_MESSAGES,
payload: { messages }
})
In component I am saving data like this:
this.props.saveMessages(data)
and also the connect:
const mapStateToProps = state => ({
author: state.chat.author,
messages: state.chat.messages,
message: state.chat.message
})
export default connect (mapStateToProps, { saveAuthor, saveMessages, deleteAuthor, deleteMessage })(Chat);
In combineReducer i.e index.js:
import {combineReducers} from 'redux';
import users from './loginReducer'
import allusers from './userReducer'
import chatReducer from './chatReducer'
export default combineReducers({
users: users,
allusers: allusers,
chat: chatReducer
})
Now if I do console.log(this.props) see screenshot below:
Now if I do console.log(this.props.messages) see screenshot below:
Now I want to map over messages data and display it but I am getting error if I do this.props.messages.messages[0] -> error this.props.messages[0] gives undefined.
Screenshot: (redux tools)

I think first you can check if this.props.messages.messages is not undefined and then you can use map() to print messages like this:
{this.props.messages && this.props.messages.messages && this.props.messages.messages.map(function(msg,i) {
return (
<p>{msg.message}</p>
)
})}

Related

Accessing state change from redux inside props, state successfully changes but props for that object is undefined

first questioner here!
I'm new to React and find it confusing to manage state with redux. From the redux-logger output, it seems that I am successfully changing the redux state regarding a user sign-in but I don't really know how to set it to props, and as such, I'm getting an undefined value for currentUser (which is the prop I want to manage across all my pages). I'm using both withRouter and Redux in an effort to pass user properties to app.js.
It starts with an API call to the backend to see if the user can login, if success then returns an object {isAdmin: "", uId: ""}.
import React from "react";
import { withRouter } from "react-router-dom";
import { setCurrentUser } from "../../redux/user/user-actions";
import { connect } from "react-redux";
// sign-in.jsx
class Login extends React.Component {
constructor(props) {
super(props);
}
onSubmitClick = async (e) => {
e.preventDefault();
fetch("/api/login", {
method: "post",
body: JSON.stringify({
email: "",
password: "",
}),
})
.then((res) => res.json())
.then((user) => {
if (user.error) {
this.setState({ error: user.error });
} else {
// Set the user in redux too:
this.props.dispatch(setCurrentUser(user));
// Redirect to main page after login
this.props.history.push({
pathname: "/",
search: "?uid=" + user.key + "?admin=" + user.admin,
state: { userId: user.key, isAdmin: user.admin },
});
}
});
};
render() {
return (...)
}
const mapStateToProps = ({ user }) => ({
currentUser: user.currentUser,
});
export default connect(mapStateToProps)(withRouter(Login));
The line with code: this.props.dispatch(setCurrentUser(user)); successfully changed the state but not the props value.
Here is the redux stuff:
// user-actions.js --------------------------------------------------------------------------------------
export const setCurrentUser = (user) => ({
type: "SET_CURRENT_USER",
payload: user,
});
// user-reducer.js --------------------------------------------------------------------------------------
// The initial state is basically a null user (ID)
const initialState = {
user: null,
};
/*
This is essentially a function that takes the current state
and action as an argument and returns a new state result.
i.e. (state, action) => newState
*/
const userReducer = (state = initialState, action) => {
// Conditional for the current action type
if (action.type.localeCompare("SET_CURRENT_USER") === 0) {
// Return a new state object
return {
// Which has the existing data but also..
...state,
// The new user object (just an ID at this point)
user: action.payload,
};
} else {
// Otherwise we return the state unchanged
// (usually when the reducer doesnt pick up the certain action)
return state;
}
};
export default userReducer;
// store.js --------------------------------------------------------------------------------------
import { createStore, applyMiddleware } from "redux";
/*
Useful for debugging redux --> logger
Is a logger middleware that console.logs the actions fired and change of state
*/
import logger from "redux-logger";
import rootReducer from "./root-reducer";
const middlewares = [logger];
const store = createStore(rootReducer, applyMiddleware(...middlewares));
export default store;
// root-reducer.js --------------------------------------------------------------------------------------
import { combineReducers } from "redux";
import userReducer from "./user/user-reducer";
export default combineReducers({
user: userReducer,
});
And finally, the App.js relevant code
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
...props,
u_id: null,
};
}
unsubscribeFromAuth = null;
componentDidMount() {
const { setCurrentUser } = this.props;[enter image description here][1]
const userState = this.props.location;
console.log(this.props);
// Make sure that state for a user isnt undefined
if (userState.state) {
this.unsubscribeFromAuth = true;
const user = userState.state.userId;
this.props.dispatch(setCurrentUser(user));
}
console.log(this.props);
}
componentWillUnmount() {
this.unsubscribeFromAuth = false;
}
render() {
return (...)
}
}
const mapStateToProps = (state) => ({
currentUser: state.currentUser,
});
//Access the state and dispatch function from our store
const mapDispatchToProps = (dispatch) => ({
setCurrentUser: (user) => dispatch(setCurrentUser(user)),
dispatch,
});
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App));
Console output with redux-logger:
https://i.stack.imgur.com/r9JyV.png
As you can see, currentUser is undefined but all props in the location are there, I'm probably making some really dumb mistake when setting currentUser with the setCurrentUser action, both in the login and then again in the componentDidMount in the app.jsx
I'll add more detail upon request
Any help would be appreciated GREATLY! :)
You are saving the user in redux under user but you are trying to access it in the mapStateToPRops via currentUser:
const mapStateToProps = (state) => ({ currentUser: state.currentUser, });
Change it to const mapStateToProps = (state) => ({ currentUser: state.user, });
and it should work.
Also this:
const mapDispatchToProps = (dispatch) => ({
setCurrentUser: (user) => dispatch(setCurrentUser(user)),
dispatch,
});
is equivalente to:
const mapDispatchToProps = ({
setCurrentUser
});
https://react-redux.js.org/using-react-redux/connect-mapdispatch#defining-mapdispatchtoprops-as-an-object

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

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

React/Redux: Why are my my properties in this.props undefined?

I am trying to set up Redux in React for the first time and I can't seem to pass my initial state from the store to the component. My store file is setting state to the return value of the reducer. Here is what happens when I log this.props to the console
Component
import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { exampleAction } from '../../actions';
class Header extends React.Component {
constructor(props) {
super(props);
this.state = {}
}
render() {
console.log(this.props)
return (
<div>
<p>this is {this.props.examplePropOne}</p>
</div>
);
}
}
const mapStateToProps = state => ({
examplePropOne: state.examplePropOne,
examplePropTwo: state.examplePropTwo
});
const mapDispatchToProps = dispatch => {
return bindActionCreators({ exampleAction }, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(Header);
Reducer
import { EXAMPLE_ACTION } from './../actions/types'
const initialState = {
examplePropOne : 'Example Property One',
examplePropTwo : 'Example Property Two'
}
export default function (state = initialState, action) {
switch(action.type) {
case EXAMPLE_ACTION:
return {
...state,
examplePropOne: action.payload
}
default:
return state
}
}
Action
import { EXAMPLE_ACTION } from './types'
export const exampleAction = text => ({
type: EXAMPLE_ACTION,
payload: text,
})
[Edit]
Here is what happens when I log the state within mapStateToProps
import React from 'react';
import { createStore, combineReducers } from 'redux';
import reducers from '../reducers';
export const store = createStore(
combineReducers({
state: reducers
}),
);
With how combineReducers() was used with state passed in as a key, your mapStateToProps() would need to look like this instead to access examplePropOne and examplePropTwo:
const mapStateToProps = state => ({
examplePropOne: state.state.examplePropOne,
examplePropTwo: state.state.examplePropTwo
});
Given that combineReducers():
The state produced by combineReducers() namespaces the states of each
reducer under their keys as passed to combineReducers()
The issue is that:
export const store = createStore(
combineReducers({
state: reducers
}),
);
The key state passed to combineReducers() created a namespace/property of state. With the argument named state for the mapStateToProps(), requires that properties are accessed as state.state. This can probably be resolved by instead giving the key passed to combineReducers() a more descriptive name representing what is being used to manage in the store. For example, if it's related to authentication, it could be called some like auth. It would look like:
export const store = createStore(
combineReducers({
auth: reducers
}),
);
// ...
const mapStateToProps = state => ({
examplePropOne: state.auth.examplePropOne,
examplePropTwo: state.auth.examplePropTwo
});
Hopefully that helps!

Generic Reducers/Actions in React/Redux

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
/* ------------------------------------------------- */

Categories

Resources