I am using react, redux and redux-saga for my project. While fetching a list of users using Redux-saga, I can see that my redux store is getting updated (I can see it from the redux dev tool), But in the component , props are not changing.
I am using a button to get the list of users. And the users are showing up in that component only.
App.js
import React, { Component } from 'react';
import { connect } from "react-redux";
import './App.css';
import { fetchUsers } from "./actions";
import { Row, Col, ListGroup, ListGroupItem } from "reactstrap";
class App extends Component {
// eslint-disable-next-line no-useless-constructor
constructor(props){
super(props);
}
render() {
console.log("in render func");
console.log(this.props);
return (
<div className="App">
<h2>Redux Saga App</h2>
<Row>
<Col sm={6}>
{this.props.userList?this.props.userList.map((user)=>(
user.first_name + user.last_name
)) : ''}
</Col>
</Row>
<button onClick={this.props.getUserList}>Click to get the users</button>
</div>
);
}
}
const mapStateToProps = (state)=>{
console.log("in map statetpprop");
//console.log(state.userList);
return {userList:state.userList}
}
const mapDispatchToProps = (dispatch)=>{
return {getUserList :() => {dispatch(fetchUsers())}}
}
App = connect(mapStateToProps,mapDispatchToProps)(App);
export default App;
action.js
export function fetchUsers(){
return {
type:'FETCH_USERS',
}
}
reducer.js
const initialState = {
userList:[]
}
export function userReducer(state=initialState,action){
switch(action.type){
case 'ADD_USER':
return Object.assign(state,{
user:action.data
})
case 'SET_USERS':
return Object.assign(state, {userList : action.data});
default:
return state;
}
}
saga.js
import {call, put , takeEvery , takeLatest } from 'redux-saga/effects';
import axios from 'axios';
import { setUsers } from "./actions";
export function fetchUsersFunc(userId){
return axios.get('https://reqres.in/api/users');
}
function* fetchUsers(action){
const users = yield call(fetchUsersFunc);
console.log("in fetch users");
if(users.data.data){
const userList = users.data.data;
console.log(userList);
console.log(userList[0].first_name)
yield put(setUsers(userList));
}
}
export function* rootSaga(){
yield [
takeLatest('FETCH_USERS',fetchUsers)
];
}
Thanks for the help!
If you use Object.assign and you want to make a new copy of the state, you need to make the target object to a new empty object instead of what you are currently doing (it mutates the state object, which makes the react unable to re-render). (You can see See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign for more information on Object.assign) For example:
// copy previous state and new updates to a new empty object
return Object.assign({}, state, {userList : action.data});
I would recommend using the spread operator instead of Object.assign though:
return {...state, userList : action.data}
If state is updated but UI is not, it means something went wrong with reducer.
Checkout reducer function carefully.
If you are using spread operator (...) in reducer function, make sure the updated data is mentioned explicitly after spread operator.
Example of working reducer function is as below:
const UvNumberReducer = (state=initialState, action: UVAction) => {
switch(action.type) {
case UV_NUMBER.LOAD:
return {
...state,
data: {
title: action.data.title,
subtitle: action.data.subtitle
}
}
default:
return state;
}
}
Related
I have a redux State HOC to manage the connection
I Have a problem when I add a new post to the store
import React, { useEffect } from "react";
import { connect } from "react-redux";
export default function withState(WrappedComponent) {
function mapStateToProps(reduxState) {
let state = {};
for(let t of Object.entries(reduxState)) {
state = {...state, ...t[1]}
}
return {
...state,
};
}
return connect(
mapStateToProps,
null
)(function (props) {
useEffect(() => {}, [props.posts, props.comments]) /*tried this but didn't work*/
return (
<React.Fragment>
<WrappedComponent {...props} />
</React.Fragment>
);
});
}
I am trying to make the program render the response from my back-end without me reloading the page manually
I tried using the useEffect
and I saw through the dev tools that the state change correctly
my reducer
import { GET_ALL_POSTS, CREATE_NEW_POST } from "../actions"
const initialState = {
posts: []
}
export default function postReducer(state = initialState, action) {
let newState = {...state}
switch(action.type){
case GET_ALL_POSTS:
return {
...newState,
posts: [...action.posts],
}
case CREATE_NEW_POST:
const posts = [...newState.posts, action.post]
return {
...newState,
posts
}
default:
return {
...newState,
}
}
}
I also read that react changes doesn't respond to shallow copies so I changed the whole array in the post reduces when I add a new post
Your withState HOC is very strange. I'm not sure why you don't just use connect directly (or use hooks). But try this:
export function withState(WrappedComponent) {
return connect(
(state) => ({
posts: state.postsReducer.posts,
comments: state.commentsReducer.comments
}),
null
)(WrappedComponent);
}
I'm new using Redux, and I'm trying to integrate React with Redux. What I want is to put all my actions in one reducer. Actually my reducer looks like this:
import {GET_ALL_CONNECTIONS, DELETE_CONNECTION, POST_CONNECTION} from '../actions';
const initialState = {
}
export default (state = initialState, { type, payload }) => {
switch (type) {
case GET_ALL_CONNECTIONS:
return payload
case POST_CONNECTION:
return {...state, ...payload}
case DELETE_CONNECTION:
return {...state, ...payload}
default:
return state
}
}
The problem is when I call the action corresponding to the GET_ALL_CONNECTIONS type:
export const getAllConnections = () => {
return async (dispatch) =>{
const response = await Conexiones.get('/db/myConnections');
dispatch({type: GET_ALL_CONNECTIONS, payload: response.data});
}
}
When I call this function in a React component is supposed to get multiple connections from an API, and save the array of object resultant of the API call in the state.
The problem is when I want to save the connections array in the state, to later map that state and generate options with each one of the connection inside a select element. When I render the component it throws me the next error:
TypeError: this.props.conexiones.map is not a function
The file where I combine all reducers looks like this:
import {combineReducers} from 'redux';
import {reducer as formReducer } from 'redux-form';
import postUser from './postUser';
import postConnection from './postConnection';
import getAllConnections from './getAllConnections';
import ConnectionsReducer from './ConnectionsReducer';
export default combineReducers({
newUser: postUser,
form: formReducer,
conexiones: ConnectionsReducer
});
And the component where I do the call looks like this:
import React, { Component } from 'react';
import { Grid, Container, Select, Button, withStyles, FormControl, InputLabel, MenuItem } from '#material-ui/core';
import {connect} from 'react-redux';
import {reduxForm, Field} from 'redux-form';
import {deleteConnection, getAllConnections} from '../actions';
const styles = theme => ({
root: {
display: 'flex',
flexWrap: 'wrap',
},
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
selectEmpty: {
marginTop: theme.spacing(2),
},
});
class BorrarConexion extends Component {
componentDidMount() {
this.props.getAllConnections();
}
handleSubmit = ({conexionId}) => {
this.props.deleteConnection(conexionId);
}
renderConexiones = () => {
return this.props.conexiones.map(conexion =>{
return (<MenuItem key={conexion.id} value={conexion.id}>{conexion.connectionUrl}</MenuItem>);
});
}
renderSelectField = ({input,label,meta: { touched, error },children,...custom}) =>{
return (
<FormControl>
<InputLabel>Seleccione la URL que desea eliminar</InputLabel>
<Select {...input} {...custom}>
{this.renderConexiones()}
</Select>
</FormControl>
)
}
render() {
return (
<Container>
<Grid container direction="column">
<Field name="conexionId" component={this.renderSelectField} label="Favorite Color"/>
<Button onClick={this.props.handleSubmit(this.handleSubmit)}>Eliminar</Button>
</Grid>
</Container>
);
}
}
const mapStateToProps = (state) => {
return {conexiones: state.conexiones}
}
const BorraConexionEstilizado = withStyles(styles)(BorrarConexion);
const formWrapped = reduxForm({form: 'delete_connection'})(BorraConexionEstilizado);
export default connect(mapStateToProps, {getAllConnections, deleteConnection})(formWrapped);
When I do this with a separate reducer called getAllConnections and replace the conexiones: ConnectionsReducers with conexiones: getAllConnections it works. The getAllConnections reducer looks like this:
export default (state = [], { type, payload }) => {
switch (type) {
case 'GET_ALL_CONNECTIONS':
return payload
default:
return state
}
}
I want to know how to do this work with one reducer receiving all my actions instead of a individual reducer for each action.
This issue is related to the fact that you are likely returning different structures from your reducer. It looks like the reducer is meant to handle objects as state but for this one case you return an array. Objects do not have a map function. You need to determine the structure of your state for this reducer and stick with it, you cannot change from array to object and back again, that is undeterministic behavior that redux is not built for.
I do not know the implementation details of your APIs but this is the main area in need of updating
const initialState = []
export default (state = initialState, { type, payload }) => {
switch (type) {
case GET_ALL_CONNECTIONS:
return payload //hoping that your api gave an array here
case POST_CONNECTION:
//return {...state, ...payload} //this is clearly returning an object
return [...state, payload] //now it returns an array with a new item
case DELETE_CONNECTION:
//return {...state, ...payload} //this is clearly returning an object
return state.filter(item => item.id !== payload.id) //now it returns a filtered array
default:
return state
}
}
The following code should recieve states as arrays and return updated states as arrays.
You have to set initial state... right now, your state is an empty object
I solved this issue by surrounding the conexion:state.conexion in the mapStateToProps method with Object.values() like this:
conexion: Object.values(state.conexion)
Thank you for stopping by to help. I am working with a react/redux app. One of the component is using a lifecyle method to retrieve data from an API. Once recieved, the data JSON data is held within an array. My initialState for the data coming back is an empty array.
When the component listening to the state change is mounted, the data is rendered on to the page, but then 2 seconds later I am getting a
Uncaught TypeError: jobs.map is not a function
Component making the API call using lifecyle method and listening for state change
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getJobs } from '../../actions';
import { Card, Grid, Image, Feed } from 'semantic-ui-react';
// import './home.css';
const renderJobs = jobs => jobs.map((job, i) => (
<Card.Group stackable key={i}>
<Card className="jobscard">
<Card.Content>
<Card.Header href={job.detailUrl} target="_blank">{job.jobTitle}</Card.Header>
<Card.Meta>{job.location}</Card.Meta>
<Card.Description>{job.company}</Card.Description>
</Card.Content>
</Card>
</Card.Group>
));
class GetJobs extends Component {
componentDidMount() {
this.props.getJobs();
}
render() {
const { jobs } = this.props;
return (
<div className="getjobs">
{renderJobs(jobs)}
</div>
);
}
}
export default connect(({ jobs }) => ({ jobs }), { getJobs })(GetJobs);
Action creator/action
export const getJobsRequest = () => fetch('https://shielded-brushlands-43810.herokuapp.com/jobs',
)
.then(res => res.json());
// action creator
export const getJobs = () => ({
type: 'GET_JOBS',
payload: getJobsRequest(),
});
Reducer
import initialState from './initialState';
export default function (jobs = initialState.jobs, action) {
switch (action.type) {
case 'GET_JOBS_PENDING':
return { ...jobs, isFetching: true };
case 'GET_JOBS_FULFILLED':
return action.payload;
case 'GET_JOBS_REJECTED':
return jobs;
default:
return jobs;
}
}
And intial state
export default {
userData: {},
jobs: [],
}
enter image description here
any thoughts on why this is happening?
You can put a simple check to ensure that your jobs is ready before you attempt rendering it.
{jobs.length && renderJobs(jobs)}
I'm in the process of learning React/Redux and I've run into an issue while converting a single page web app of mine to the framework/paradigm. What i'm trying to do is let the initial state of the web app have an array that is to be populated by objects from an API request, this array is called "makes". I want to do this so I can display "makes" from the API on the first page of the website upon it loading. This can be seen in the index.js file below:
import App from './App';
import './index.css';
import configureStore from './redux/store'
import { Provider } from 'react-redux'
let makesUrl = 'the url to the API'
let cached = cache.get(makesUrl)
let makes = []
// Future cache setup.
if(!cached) {
console.log("NOT CACHED")
}
else {
console.log("CACHED")
}
// Get the makes via API.
fetch(makesUrl).then((response) => {
// Pass JSON to the next 'then'
return response.json()
}).then((json) => {
json.makes.forEach((make) => {
makes.push(make)
})
})
let initialState = {
grids: [],
makes: makes
}
let store = configureStore(initialState)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
The state and dispatch are mapped to the props and passed down to the components that need them in my App.js file as such:
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import './App.css'
import Head from './components/Head'
import Middle from './components/Middle'
import Foot from './components/Foot'
import actions from './redux/actions'
class App extends Component {
render() {
return (
<div className="App">
<div>
<Head />
<div className="middle container">
<Middle actions={this.props.actions} makes={this.props.makes}/>
</div>
<Foot />
</div>
</div>
);
}
}
function mapStateToProps(state) {
return state
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App)
At all points, in the chrome dev tools, I can see that the API call was successful and the state is shown to have makes: Array[62] with 62 objects inside, however if I console log the length of the array in the component that these makes are passed down to as seen below, it says the length is 0, and each index of the array is undefinded.
import React, { Component } from 'react'
class MakeButtons extends Component {
handleClick(event) {
event.preventDefault()
console.log("CLICK")
}
render() {
return(
<div>
{
console.log(this.props.makes.length)
}
</div>
)
}
}
export default MakeButtons
This is essentially what I've been trying to figure out for the past couple hours, so I can use the forEach or map function to return links/buttons for each of the objects in the array, however at the moment this does not work, despite dev tools showing the state to be normal. Any help/explanations would be greatly appreciated!
So you really just need to set up an action/reducer for your init, then you can call it in componentWillMount or componentDidMount because they are only called once upon loading your app.
In the way you are doing it now you have a fetch and an app using the data from the fetch that is not waiting for the async call to finish before it starts the app.
You just want to create your init action, so your action creator would be something like :
import * as services from './services';
function initMyAppDispatcher(type, data, status) {
return {
type,
data,
status
};
}
export function initMyApp(dispatch, makesUrl) {
dispatch(initMyAppDispatcher(actions.INIT_APP, {}, 'PENDING');
return services.myInitCall(makesUrl)
.then((data) =>
dispatch(initMyAppDispatcher(actions.INIT_APP, data, 'SUCCESS'),
(error) =>
dispatch(initMyAppDispatcher(actions.INIT_APP, error, 'FAILURE'),
)
.catch(yourErrorHandling);
}
Services.myInitCall is however you want to implement it, just make sure you export it back as a promise. In your case you can replace that line with fetch(makesUrl) as long as you have access to it there. Then having it set up like this, you can set your reducers like so :
case actions.INIT_APP:
if (action.status) {
switch (action.status) {
case PENDING:
//you can use this to create a "loading" state like a spinner or whatever
return state;
case SUCCESS:
// note: this is immutablejs syntax, use whatever you prefer
return state.set('makes', action.data);
case FAILURE:
return state;
default:
return state;
}
}
return state;
One thing to note is I have dispatch in my action creators because I use mapDispatchToProps in place of mapToProps. So your container looks something like this :
import * as actionCreators from './action-creators';
function mapStateToProps(state) {
return {
makes: state.get('makes')
};
}
function mapDispatchToProps(dispatch, ownProps) {
return {
initMyApp: actionCreators.initMyApp.bind(null, dispatch)
};
}
export default function(component = Component) {
return connect(mapStateToProps, mapDispatchToProps)(component);
}
then in your component componentWillMount or componentDidMount, pass in and call your init function
componentDidMount() {
this.props.initMyApp();
}
Im new to React and Redux and still kinda confused a little bit.
My goal is to render a bunch of json datas in the HTML by using GET request. I'm using react and redux to manage the state of the objects, but I believe my problem is that the data is not even there
so basically whenever someone request a URL /courses , he/she will see bunch of data in json.
I get the error in the component
TypeError: Cannot read property 'map' of undefined
Here's the code
Action
export function getCourses() {
return (dispatch) => {
return fetch('/courses', {
method: 'get',
headers: { 'Content-Type', 'application/json' },
}).then((response) => {
if (response.ok) {
return response.json().then((json) => {
dispatch({
type: 'GET_COURSES',
courses: json.courses
});
})
}
});
}
}
Reducer
export default function course(state={}, action) {
switch (action.type) {
case 'GET_COURSES':
return Object.assign({}, state, {
courses: action.courses
})
default:
return state;
}
}
Component
import React from 'react';
import { Link } from 'react-router';
import { connect } from 'react-redux';
class Course extends React.Component {
allCourses() {
return this.props.courses.map((course) => {
return(
<li>{ course.name }</li>
);
});
}
render() {
return (
<div>
<ul>
{ this.allCourses() }
</ul>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
courses: state.courses
}
}
export default connect(mapStateToProps)(Course);
Index reducer, where i combine everything
import { combineReducers } from 'redux';
import course from './course';
export default combineReducers({
course,
});
Configure Store , where i store the intial state and the reducer
import { applyMiddleware, compose, createStore } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
export default function configureStore(initialState) {
const store = createStore(
rootReducer,
initialState,
compose(
applyMiddleware(thunk),
typeof window == 'object' && typeof window.devToolsExtension !== 'undefined' ? window.devToolsExtension() : f => f
)
);
return store;
}
I believe why the data is not there is because i didn't call the action? any help would be appreciated.
mapStateToProps takes the root state as an argument (your index reducer, which is also the root reducer), not your course reducer. As far as I can tell this is the structure of your store:
-index <- This is the root reducer
-course
So to get the courses from that state, in your component:
// state is the state of the root reducer
const mapStateToProps = (state) => {
return {
courses: state.course.courses
}
}
Also, you might consider initialising the state of the course reducer with an empty array of courses, so if you have to render the component before the action is fired, you won't get the error.
const initialState = {
courses: []
};
export default function course(state= initialState, action) {
...
}
Finally, you're not firing the action at all, so you will never actually get the courses, I assume you want them to be retrieved once the Course component is loaded, for that you can use the componentDidMount event in your component.
First of all, you need to map the action to a property of the component
// Make sure you import the action
import { getCourses } from './pathToAction';
...
const mapDispatchToProps = (dispatch) => {
return {
onGetCourses: () => dispatch(getCourses())
};
}
// Connect also with the dispatcher
export default connect(masStateToProps, mapDispatchToProps)(Course);
Now call the onGetCourses property when the component mounts
class Course extends React.Component {
componentDidMount() {
this.props.onGetCourses();
}
...
}
its because props sometime can be undefined so you have to write a condtion like this
allCourses() {
if(this.props.courses){
return this.props.courses.map((course) => {
return(
<li>{ course.name }</li>
);
});
}
else {
return [];
}