Hello, I am new to redux and I am struggling with a problem. I am trying to access and map over the comments within my post array. However, I am not sure how to do this. So far, I've tried changing the actions and reducers in order to solve this issue. I think the problem is within the react and redux. I can't tell if my mapStateToProps is working correctly. Also, the state is being fetched from my express server and it seems to be working properly as you can see in the picture.
My getPost action:
export const getPost = (group_id, post_id) => async dispatch => {
try {
const res = await axios.get(`/api/groups/${group_id}/${post_id}`);
dispatch({
type: GET_POST,
payload: res.data
});
} catch (error) {
dispatch({
type: POST_ERROR,
payload: { msg: error.response.statusText, status: error.response.status }
});
}
};
The initial state:
const initialState = {
groups: [],
group: [],
loading: true,
error: {}
};
The reducer:
case GET_POST:
return {
...state,
post: payload,
loading: false
};
Where I'm trying to map over the comments:
import React, { Fragment, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getPost } from '../../../redux/actions/group';
const Post = ({ getPost, post, match }) => {
useEffect(() => {
getPost(match.params.group_id, match.params.post_id);
}, [getPost, match.params.group_id, match.params.post_id]);
// I want to map over the comments here
return (
{post.comments.map(comment => ({ comment }))}
);
};
Post.propTypes = {
getPost: PropTypes.func.isRequired,
group: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
post: state.post
});
export default connect(mapStateToProps, { getPost })(Post);
You can access nested object with some tricks using redux, we have use this way in our prod env for some time.
First the reducer (you can make this reducer even more complex)
const LocalStorageReducer = createReducer<Store['localStorage']>(
new LocalStorage(),
{
saveLocalStorageItem(state: LocalStorage, action: any) {
return {...state, [action.payload.item]: action.payload.value}; // <= here
},
}
);
For Actions
export const actions = {
saveLocalStorageItem: (payload: InputAction) => ({type: 'saveLocalStorageItem', payload}),
};
For the type InputAction
export class InputAction {
item: string;
value: string | Array<string> | null | boolean;
constructor() {
this.item = '';
this.value = null;
}
}
For the handler in component
this.props.saveLocalStorage({ item: 'loading', value: false });
In this way you can go one way done to the nested redux store.
For complex (4-5 levels) and multiple (> 2 times) data structure, there are other ways, but in most situations, it's good enough.
Related
I have a response from my mongodb database as the following
{_id: '61ca4273e7cc1da1f3dbc9a3', title: 'Hero Syndrome', slug: 'hero-syndrome', category: 'Game', release_date: null, … }
I'm using Redux to fetch the data.
When I do console.log(game) which is the Object I provided, the console return the Object indeed. But when I'm trying to access the children such as title or slug it doesn't work.
I used to have this error Objects are not valid as a React child .. but fixed it somehow randomly in the code.
Any idea how to access title for example ?
What I tried : {title}, title, {game.title} and none of them work
What I did to get data from Redux :
GameComponent.propTypes = {
game: PropTypes.object.isRequired,
};
const mapStateToProps = state => ({
game: state.game,
});
And at the top of the component
function GameComponent({
game: { game, loading, title },
}) { ....
I tried to convert the object to string and to array in order for React to read it but I failed.
Code :
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { getGameByToken } from '../actions/game';
import GameOverview from './GameOverview';
function GameComponent({ game: { game, loading, title }, getGameByToken, auth }) {
useEffect(() => {
getGameByToken(token);
}, [getGameByToken, token]);
return <>
//this doesn't work
Title : {title}
</>;
}
GameComponent.propTypes = {
getGameByToken: PropTypes.func.isRequired,
game: PropTypes.object.isRequired,
auth: PropTypes.object.isRequired,
};
const mapStateToProps = (state) => ({
game: state.game,
auth: state.auth,
});
export default connect(mapStateToProps, { getGameByToken })(GameComponent);
Response from database in redux :
import axios from 'axios';
import { setAlert } from './alert';
import { GET_GAME, GAME_ERROR } from './types';
// GET GAMES BY TOKEN
export const getgameByToken = (token) => async (dispatch) => {
try {
const res = await axios.get('/api/games/' + token);
dispatch({ type: GET_GAME, payload: res.data });
} catch (err) {
dispatch({
type: GAME_ERROR,
payload: {
msg: err.response.msg,
status: err.response.status,
},
});
}
};
From Redux Dev Tools :
EDIT: If I rename game: state.game, to game: state.game.game, I actually got the value ! But when refreshing It goes back saying TypeError: Cannot read properties of null (reading 'title')
I am using React Router, Redux, Redux Sagas, antd in my project.
My main goal is to click on a cell from the table of items, go to the next page, which is Details page for that item, and fetch details from server using ID that I pass in from the Link. It is not working right now.
I have a component, component A, which has column like this:
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
render: (id: number) => <Link to={{ pathname: `/pages/my-page/${id}`, state: {id}}}>{id}</Link>,
},
It goes to the next component, Component B, with the param id passed in. Now, I want to use that id to do further fetching of details for that particular item, using redux and redux saga and render it on the next page, Component B, which I do using constructor. I tried using this below code in componentDidMount() as well, but that doesn't work either.
constructor(props: any) {
super(props);
const {onDetailsSearch, state} = props;
const {router} = state;
const {location} = router;
const {state: newState}: {state: any} = location;
onDetailsSearch({id: (newState || {}).id});
}
This is what is in the reducer:
import {DetailsState} from 'state/'
import actions, {DetailsAction} from './actions'
export interface DetailsState {
loading: boolean
id: number
items: Details[]
}
const initialState: DetailsState = {
loading: false,
id: 0,
items: [],
}
export default function DetailsReducer(state: DetailsState = initialState, action: DetailsAction): DetailsState {
switch (action.type) {
case actions.SET_STATE:
return { ...state, ...action.payload }
default:
return state
}
}
This is the Redux saga code:
import {all, call, put, takeEvery} from 'redux-saga/effects'
import getDetails from "services/getDetails";
import actions from './actions'
type Params = {
type: typeof actions.LOAD_DETAILS
payload: object
}
export function* LOAD_DETAILS({ payload }: Params) {
yield put({
type: 'details/SET_STATE',
payload: {
loading: true,
},
})
const { response, error } = yield call(getDetails, payload)
console.log('response:', response);
if (!error) {
yield put({
type: 'details/SET_STATE',
payload: {
details: response.data,
},
})
}
else {
// TODO: handle error
}
yield put({
type: 'details/SET_STATE',
payload: {
loading: false,
},
})
}
export default function* rootSaga() {
yield all([
takeEvery(actions.LOAD_DETAILS, LOAD_DETAILS),
])
}
This is how I call the dispatch function onDetailsSearch, which in turn should call the saga and set the results in the redux store.
But its not doing that. What is wrong here?
I found out what the problem was. I have actions stored as constants in a actionConstants.ts file.
The action constant for this particular action was 'details/LOAD_DETAILS', and the action string in dispatch I was using was 'LOAD_DETAILS'
First, I made a small application on the React.js. Using the fetch method, I take the API
And these are the main files of my application:
Index.js:(action)
export const SHOW_AIRPLANES = "SHOW_AIRPLANES";
export function showAirplanes() {
return (dispatch, getState) => {
fetch("https://api.iev.aero/api/flights/25-08-2019").then(response => {
dispatch({ type: SHOW_AIRPLANES, payload: response.data });
});
};
}
airplanes.js:(reducer)
import { SHOW_AIRPLANES } from '../actions'
const initialState = {
list: []
}
export function showAirplanes(state = initialState, action) {
switch (action.type) {
case SHOW_AIRPLANES:
return Object.assign({}, state, {list: action.payload})
default:
return state
}
}
index.js(reducer):
import { combineReducers } from "redux";
import { showAirplanes } from "./airplanes";
const rootReducer = combineReducers({
user: showAirplanes
});
export default rootReducer;
First, you should use the createStore function like so:
const initialData = {}; // whatever you want as initial data
const store = createStore(reducers, initialData, applyMiddleware(thunk));
Then pass it to your provider
<Provider store={store}>
{...}
</Provider
next, when you map your reducers inside the combineReducers function, each key in this object represents a piece of your state. So when you do user: showAirplanes it means that you intend to use it in the mapStateToProps with state.user.list so I think you meant to call it airplane: showAirplanes.
Then, your reducer name is not informative enough, I would suggest to change it to airplanesReducer.
Next issue, the call to fetch returns a response that has JSON that must be resolved.
Change this:
fetch("https://api.iev.aero/api/flights/25-08-2019").then(response => {
dispatch({ type: SHOW_AIRPLANES, payload: response.data });
});
To this:
fetch("https://api.iev.aero/api/flights/25-08-2019")
.then(res => res.json())
.then(response => {
dispatch({ type: SHOW_AIRPLANES, payload: response.body.departure });
});
Note that I've changed the value that you need to resolve from the response as well.
Inside your App.js component you need to create a constructor and bind the renderAirplaneList function to this
// Inside the App class
constructor(props) {
super(props);
this.renderAirplaneList = this.renderAirplaneList.bind(this);
}
And finally (I hope I didn't miss anything else), you map your state in the App.js component to { airplanes: state.airplanes.list} so the name of the prop you expect inside your component is props.airplanes.
renderAirplaneList() {
if (!this.props.airplanes.length) {
return null;
}
const arr = this.props.airplanes || [];
return arr.map(airplane => {
return (
<tr key={airplane.id}>
<td>{airplane.ID}</td>
<td>{airplane.term}</td>
<td>{airplane.actual}</td>
<td>{airplane["airportToID.city_en"]}</td>
</tr>
);
});
}
Make sure you go over the documentation of React and Redux, they have all the information you need.
Good luck.
aren't you suppose to send some parameters to this call?
this.props.showAirplanes()
it seems that it has 2 parameters: state and action, although state seems to have already it's default value
TypeError: Cannot read property '' of undefined ı have no idea why ı am getting this error while I do check the code below everything seems fine :( trying to learn the way how react works :)
So what is the purpose of this since all the properties I wrap on contextprovider suchas contacts loading and the functions I need
import React, { useState, useContext } from 'react'
import ContactContext from '../context/contactContext'
export default function ContactForm() {
const name = useFormInput('')
const email = useFormInput('')
const contactContext = useContext(ContactContext)
const { addContact } = contactContext
const onSubmit = () => {
addContact(name.value, email.value)
name.onReset()
email.onReset()
}
return (
SOME HTML CODE HERE
)
}
//contactState.js
import React, { useReducer } from 'react'
import _ from 'lodash'
import ContactContext from './contactContext'
import ContactReducer from './contactReducer'
const ContactState = props => {
const initialState = {
contacts: [
{
id: '098',
name: 'Diana Prince',
email: 'diana#us.army.mil'
}
],
loading: false,
error: null
}
const [state, dispatch] = useReducer(ContactReducer, initialState)
const [contacts, loading] = state
const addContact = (name, email) => {
dispatch({
type: 'ADD_CONTACT',
payload: { id: _.uniqueId(10), name, email }
})
}
const delContact = id => {
dispatch({
type: 'DEL_CONTACT',
payload: id
})
}
return (
<ContactContext.Provider
value={{
contacts,
loading,
addContact,
delContact
}}
>
{props.children}
</ContactContext.Provider>
)
}
export default ContactState
//contactReducer.js
export default (state, action) => {
switch (action.type) {
case 'ADD_CONTACT':
return {
contacts: [...state, action.payload]
}
case 'DEL_CONTACT':
return {
contacts: state.contacts.filter(
contact => contact.id !== action.payload
)
}
case 'START':
return {
loading: true
}
case 'COMPLETE':
return {
loading: false
}
default:
throw new Error()
}
}
//contactContext.js
import { createContext } from 'react'
const contactContext = createContext()
export default contactContext
In your reducer, when adding a contact, you're spreading the wrong state key. This should fix it:
case 'ADD_CONTACT':
return {
contacts: [...state.contacts, action.payload]
}
I can't see where you are using ContactState in your app. If you don't use it and render your ContactForm component with it then you can't reach any context value. You should render it as:
<ContactState>
<ContactForm />
</ContactState>
in a suitable place in your app. Also, you can't get contacts and loading like that:
const [ contacts, loading ] = state;
state is not an array, it is an object here. You should use:
const { contacts, loading } = state
You can find a simplified version of your code below. I removed/changed some parts in order to run it as much as possible. You should fix your reducer as #Asaf David mentioned in their answer, but this is not the main problem here. After fixing the context issue, you can try to fix your reducer.
About your questions, if you try to understand how React works by looking at this example you can easily get confused. Because Context is an advanced concept (at least for the beginners). Also, the code uses useReducer with Context and this makes the things more complicated. If your intent is to understand the React itself then start with the beginner guide.
By using Context we can pass the data top-down to the deepest components. But, in order to use that data those components should be rendered as children of the context provider.
In your code, you are doing this in ContactState but you never use it. Also, in that component, you are defining a state with useReducer and feed your context with this state by value.
Finally, in your ContactForm component, you are using useContext hook to get the context data. In your current code since you don't render this component in a provider, contactContext is undefined and you are getting the error. You can't get addContact from undefined.
In my example, I'm retrieving the contacts to show something. Again, I've changed/removed some parts from your code.
const { createContext, useContext, useReducer } = React;
const ContactContext = createContext();
function ContactForm() {
// Changed those values
const name = "";
const email = "";
const contactContext = useContext(ContactContext);
// changed addContact -> contacts
const { contacts } = contactContext;
const onSubmit = () => {
addContact(name.value, email.value);
name.onReset();
email.onReset();
};
// added some data to render
return <div>{contacts[0].name}</div>;
}
function ContactReducer(state, action) {
switch (action.type) {
case "ADD_CONTACT":
return {
contacts: [...state, action.payload]
};
case "DEL_CONTACT":
return {
contacts: state.contacts.filter(
contact => contact.id !== action.payload
)
};
case "START":
return {
loading: true
};
case "COMPLETE":
return {
loading: false
};
default:
throw new Error();
}
}
const ContactState = props => {
const initialState = {
contacts: [
{
id: "098",
name: "Diana Prince",
email: "diana#us.army.mil"
}
],
loading: false,
error: null
};
const [state, dispatch] = useReducer(ContactReducer, initialState);
const { contacts, loading } = state;
const addContact = (name, email) => {
dispatch({
type: "ADD_CONTACT",
// removed lodash part, added a static id
payload: { id: 1, name, email }
});
};
const delContact = id => {
dispatch({
type: "DEL_CONTACT",
payload: id
});
};
return (
<ContactContext.Provider
value={{
contacts,
loading,
addContact,
delContact
}}
>
{props.children}
</ContactContext.Provider>
);
};
// added the relevant render part
ReactDOM.render(
<ContactState>
<ContactForm />
</ContactState>,
document.getElementById("root")
);
<script src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root" />
In the last couple of days I have been working on my Redux api call. I am actually having a problem getting the data back to the view component. Currently I'm able to see the data in the in the action generator, so I know at least I'm able to get it. However, nothing is showing in the view. I imagine it may have something to do with when it's loading. This is why I tried to load it when the component is rendering.
https://djangoandreact.herokuapp.com/user/1 is what is not loading.
codesandbox: https://codesandbox.io/s/zlor60q3jm?from-embed
Should be able to go to /user/1 at the end similar to going to /1 brings up an article(Tough Hope)
Heres the view component:
import React from "react";
import { connect } from "react-redux";
import { fetchUser } from "../store/actions/userActions";
class UserDetailView extends React.Component {
componentDidMount() {
const userID = this.props.match.params.userID;
fetchUser(userID); //fixed
}
render() {
const { user } = this.props.user;
console.log(user);
return (
<div>
<h3>{user.username}</h3>
</div>
);
}
}
const mapStateToProps = state => ({
user: state.user
});
const mapDispatchToProps = (dispatch, ownProps) => ({
fetchUser: dispatch(fetchUser(ownProps.match.params.userID))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserDetailView);
Action generator
import axios from "axios";
import { thunk } from "react-redux";
export function fetchUser(userID) {
console.log(userID);
return dispatch => {
return axios.get(`/api/user/${userID}`).then(res => {
dispatch(fetchUserSuccess(res.data));
console.log(res.data); // loads data
});
};
}
// Handle HTTP errors since fetch won't.
function handleErrors(response) {
if (!response.ok) {
throw Error(response.statusText);
}
return response;
}
export const FETCH_USER_BEGIN = "FETCH_USER_BEGIN";
export const FETCH_USER_SUCCESS = "FETCH_USER_SUCCESS";
export const FETCH_USER_FAILURE = "FETCH_USER_FAILURE";
export const fetchUserBegin = () => ({
type: FETCH_USER_BEGIN
});
export const fetchUserSuccess = user => ({
type: FETCH_USER_SUCCESS,
payload: { user }
});
export const fetchUserFailure = error => ({
type: FETCH_USER_FAILURE,
payload: { error }
});
Reducers(which are probably fine):
import {
FETCH_USER_BEGIN,
FETCH_USER_SUCCESS,
FETCH_USER_FAILURE
} from "../actions/actionTypes";
const initialState = {
user: {},
loading: false,
error: null
};
export default function userReducer(state = initialState, action) {
switch (action.type) {
case FETCH_USER_BEGIN:
return {
...state,
loading: true,
error: null
};
case FETCH_USER_SUCCESS:
return {
...state,
loading: false,
user: action.payload.user
};
case FETCH_USER_FAILURE:
return {
...state,
loading: false,
error: action.payload.error,
user: {}
};
default:
return state;
}
}
folks. I found it.
case FETCH_USER_SUCCESS:
return {
...state,
loading: false,
user: action.payload.user
};
user is supposed to be user:action.payload
Also, the user action was supposed to be
export const fetchUserSuccess = user => ({
type: FETCH_USER_SUCCESS,
payload: user
})
WOOOOW. But, honestly, I learned so much about Redux in the last two sleepless nights, it was worth the pain. Really was. Now, instead of copy pasta, I know what an action generator is and does, and reducer (obvi)