Cannot redirect to new page on first submission of form with history.push() - node.js

Edit
I've done some more debugging and here is the problem:
CreateProfile.js calls profileActions.createProfile() and passes data to be operated on and this.props.history so that it can push a new path onto the history stack.
profileActions.createProfile() successfully sends data to database. Database successfully uses the data.
profileActions.createProfile() pushes new path onto stack. The component at the path loads and successfully calls a reducer.
The URL in the browser does not reflect the path that is pushed onto the history stack. The new component does not load.
This only happens when creating an entry in the database. When updating an entry, the program works as expected.
I'm currently trying to redirect to a new page with react/redux. On the first submission, the form submits to the backend and creates an entry in the database but fails to redirect to the next page. On the second submission, however, it redirects just fine.
I'm using this.props.history.push() to do the redirect.
I think It may be an issue with the the response received from the backend but I cannot seem to figure out what the issue is. The reason I believe this is because it is hitting different logic because on the second submission, it is updating and not creating an entry.
Here is my component (CreateProfile.js)
import React, { Component } from 'react'
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { createProfile } from '../../actions/profileActions';
import TextAreaGroup from '../common/TextAreaGroup';
import InputGroup from '../common/InputGroup';
class CreateProfile extends Component {
// Constructor
// componentWillRecieveProps()
onSubmit = (evt) => {
evt.preventDefault();
const profileData = {
handle: this.state.handle,
bio: this.state.bio,
website: this.state.website,
twitter: this.state.twitter,
instagram: this.state.instagram,
youtube: this.state.youtube,
linkedin: this.state.linkedin,
github: this.state.github,
vsco: this.state.vsco
};
this.props.createProfile(profileData, this.props.history);
}
//onChange()
render() {
// render logic
return (
// markup
<form onSubmit={this.onSubmit}>
// markup
<input
type="submit"
value="Create Profile"
className="btn btn-info btn-block mt-4"
/>
</form>
</div>
</div>
</div>
</div>
)
}
}
CreateProfile.propTypes = {
createProfile: PropTypes.func.isRequired,
profile: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
profile: state.profile,
errors: state.errors
});
export default connect(mapStateToProps, { createProfile })(withRouter(CreateProfile));
Here is my action file that submits to the backend (profileActions.js):
import axios from 'axios';
// import types
import { GET_PROFILE, PROFILE_LOADING, GET_ERRORS, CLEAR_CURRENT_PROFILE } from './types';
// Create Profile
export const createProfile = (profileData, history) => dispatch => {
axios.post('/api/profile', profileData)
.then(res => history.push('/login'))
.catch(err => {
dispatch({
type: GET_ERRORS,
payload: err.response.data
})
})
};
}
And here is the route in my backend that is being submitted to:
router.post('/', passport.authenticate('jwt', { session: false }), (req, res) => {
const { errors, isValid } = validateProfileInputs(req.body);
if (!isValid) {
return res.status(400).json(errors);
}
const profileFields = {}; //code setting fields omitted
Profile.findOne({user: req.user.id}).then(profile => {
if (profile) {
// Update Profile
Profile.findOneAndUpdate(
{ user: req.user.id },
{ $set: profileFields },
{ new: true }
).then(profile => res.json(profile)); // SUCCESSFUL PUSH ONTO THIS.PROPS.HISTORY
} else {
// Create Profile
// Check if handle exists
Profile.findOne({ handle: profileFields.handle })
.then(profile => {
if (profile) {
errors.handle = 'That handle already exists';
res.status(400).json(errors);
}
new Profile(profileFields).save().then(profile => res.json(profile)); // PUSH ONTO THIS.PROPS.HISTORY NOT OCCURRING
});
}
});
});
Any and all help would be greatly appreciated. I have tried my hardest but cannot seem to figure out what the issue is.

This problem arose because of my lack of understanding of how asynchronous javascript works.
The issue was with a few lines of code in the component that I was trying to push too.
componentDidMount() {
this.props.getProfile(); // Async function, sets profile object in store
}
render() {
const { profile } = this.state.profile;
if(!Object.keys(profile).length > 0) { // This is always evaluates to true
// because it executes before
// this.props.getProfile() returns
this.props.history.push('/create-profile');
}
}

Related

RTK Query in Redux-Toolkit is returning data of undefined, when I console.log the data it appears in console

I was trying to display an array of data fetched from my custom server with RTK Query using Next.js (React framework). And this is my first time using RTK Query. Whenever I console.log the data, it appears in the browser console. But whenever I try to map the data to render it in the browser, it keeps throwing an error saying Cannot read properties of undefined (reading 'map').
I figured Next.js always throws an error if an initial state is undefined or null even if the state change. This link talked about solving the problem using useMemo hook https://redux.js.org/tutorials/essentials/part-7-rtk-query-basics
But I didn't understand it well. Please kindly help me out with displaying the data.
Here is the BaseQuery function example I followed, it was derived from redux toolkit docmentation https://redux-toolkit.js.org/rtk-query/usage/customizing-queries#axios-basequery
import axios from "axios";
const axiosBaseQuery =
({ baseUrl } = { baseUrl: "" }) =>
async ({ url, method, data }) => {
try {
const result = await axios({ url: baseUrl + url, method, data });
return { data: result.data };
} catch (axiosError) {
let err = axiosError;
return {
error: { status: err.response?.status, data: err.response?.data },
};
}
};
export default axiosBaseQuery;
I make the GET request here
import { createApi } from "#reduxjs/toolkit/query/react";
import axiosBaseQuery from "./axiosBaseQuery";
export const getAllCarsApi = createApi({
reducerPath: "getAllCarsApi",
baseQuery: axiosBaseQuery({
baseUrl: "http://localhost:5000/",
}),
endpoints(build) {
return {
getAllCars: build.query({
query: () => ({ url: "all-cars", method: "get" }),
}),
};
},
});
export const { useGetAllCarsQuery } = getAllCarsApi;
This is my redux store
import { configureStore } from "#reduxjs/toolkit";
import { getAllCarsApi } from "./getAllCarsApi";
import { setupListeners } from "#reduxjs/toolkit/dist/query";
const store = configureStore({
reducer: { [getAllCarsApi.reducerPath]: getAllCarsApi.reducer },
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(getAllCarsApi.middleware),
});
setupListeners(store.dispatch);
export default store;
I provide the store to the _app.js file.
import "../styles/globals.css";
import axios from "axios";
import { MyContextProvider } from "#/store/MyContext";
import { Provider } from "react-redux";
import store from "#/store/ReduxStore/index";
axios.defaults.withCredentials = true;
function MyApp({ Component, pageProps }) {
return (
<MyContextProvider>
<Provider store={store}>
<Component {...pageProps} />
</Provider>
</MyContextProvider>
);
}
export default MyApp;
I get the data here in my frontend.
import { useGetAllCarsQuery } from "#/store/ReduxStore/getAllCarsApi";
const theTest = () => {
const { data, isLoading, error } = useGetAllCarsQuery();
return (
<div>
{data.map((theData, i) => (
<h1 key={i}>{theData}</h1>
))}
<h1>Hello</h1>
</div>
);
};
export default theTest;
This is a timing thing.
Your component will always render immediately and it will not defer rendering until data is there. That means it will also render before your data has been fetched. So while the data is still loading, data is undefined - and you try to map over that.
You could do things like just checking if data is there to deal with that:
const theTest = () => {
const { data, isLoading, error } = useGetAllCarsQuery();
return (
<div>
{data && data.map((theData, i) => (
<h1 key={i}>{theData}</h1>
))}
<h1>Hello</h1>
</div>
);
};

How to update Vuex state? (MEVN stack)

I am doing a school project and currently I am just trying out Vuex, I want to retrieve a list of workshops from Vuex, but I can't seem to update my state.
This is my Node backend:
router.get('/all', (req, res) => {
Workshop.find({})
.then( workshop => {
return res.status(201).json({
workshop: workshop,
success: true
})
})
.catch( err => {
console.log(err)
})
})
This is my result in Postman:
This is my Vuex store:
import axios from 'axios'
const state = {
workshop: {}
}
const getters = {
workshop: state => state.workshop
}
const actions = {
async getWorkshop({ commit }) {
let res = await axios.get('http://localhost:5000/api/workshops/all');
commit('workshop_success', res.data.workshop);
return res.data.workshop;
}
};
const mutations = {
workshop_success(state, workshop) {
state.workshop = workshop
}
};
export default {
state,
getters,
actions,
mutations
}
This is my component:
<template>
<p>{{ workshop }}</p>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
computed: {
...mapGetters(['workshop'])
},
methods: {
...mapActions(['getWorkshop'])
},
created() {
this.getWorkshop
},
}
</script>
The problem is, I am able to get the workshop state through Vuex, it displays a simple empty object "{}" (which is the initial state), but it seems like I am unable to trigger the action through the created hook, and the state does not change. If anyone has an idea of what I did wrong, that would be really helpful, because I am really lost right now. Thank you in advance!
state is not a state but context object in mutation, etc parameters. Otherwise commit, etc couldn't be accessed.
It is:
workshop_success({ state }, workshop) {
state.workshop = workshop
}
Also this is no-op:
created() {
this.getWorkshop
},
A function should be called like this.getWorkshop().

Node/React/Redux: having problems passing api JSON object between Node and React

I am new to React/redux with Node. I am working on a full stack app that utilizes Node.js on the server side and React/Redux on the client side. One of the functions of the app is to provide a current and eight-day weather forecast for the local area. The Weather route is selected from a menu selection on the client side that menu selection corresponds to a server side route that performs an axios.get that reaches out and consumes the weather api (in this case Darksky) and passes back that portion of the JSON api object pertaining to the current weather conditions and the eight-day weather forecast. There is more to the API JSON object but the app consume the "current" and "daily" segment of the total JSON object.
I have written a stand-alone version of the server-side axios "get" that successfully reaches out to the Darksky API and returns the data I am seeking. I am, therefore, reasonably confident my code will correctly bring back the data that I need. My problem consists in this: when I try to render the data in my React Component, the forecast object is undefined. That, of course, means there is nothing to render.
I have reviewed my code, read a plethora of documentation and even walked through tutorials that should help me find the problem and it still eludes me. So, I am stuck and would greatly appreciate some help. Most of the comment you still in the code below will be removed after the debugging process is completed.
I am including code blocks relevant to the problem:
My React Component
// client/src/components/pages/functional/Weather.js
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import Moment from 'react-moment';
import Spinner from '../../helpers/Spinner'
import { getWeather } from '../../../redux/actions/weather'
const Weather = ({ getWeather, weather: { forecast, loading } }) => {
// upon load - execute useEffect() only once -- loads forecast into state
useEffect(() => { getWeather(); }, [getWeather])
return (
<div id='page-container'>
<div id='content-wrap' className='Weather'>
{ loading ?
<Spinner /> :
<>
<div className='WeatherHead box mt-3'>
<h4 className='report-head'>Weather Report</h4>
</div>
{/* Current Weather Conditions */}
<h6 className='current-head'>Current Conditions</h6>
<section className='CurrentlyGrid box mt-3'>
/* additional rendering code removed for brevity */
<span><Moment parse='HH:mm'>`${forecast.currently.time}`</Moment></span>
/* additional rendering code removed for brevity */
</section>
</>
}
</div>
</div>
);
};
Weather.propTypes = {
getWeather: PropTypes.func.isRequired,
weather: PropTypes.object.isRequired
};
const mapStateToProps = state => ({ forecast: state.forecast });
export default connect( mapStateToProps, { getWeather } )(Weather);
My React Action Creator
// client/src/redux/actions/weather.js
import axios from 'axios';
import chalk from 'chalk';
// local modules
import {
GET_FORECAST,
FORECAST_ERROR
} from './types';
// Action Creator
export const getWeather = () => async dispatch => {
try {
// get weather forecast
const res = await axios.get(`/api/weather`);
console.log(chalk.yellow('ACTION CREATOR getWeather ', res));
// SUCCESS - set the action -- type = GET_WEATHER & payload = res.data (the forecast)
dispatch({
type: GET_FORECAST,
payload: res.data
});
} catch (err) {
// FAIL - set the action FORECAST_ERROR, no payload to pass
console.log('FORECAST_ERROR ',err)
dispatch({
type: FORECAST_ERROR
});
};
};
My React Reducer
// client/src/redux/reducers/weather.js
import {
GET_FORECAST,
FORECAST_ERROR,
} from '../actions/types'
const initialState = {
forecast: null,
loading: true
}
export default (state = initialState, action) => {
const { type, payload } = action
switch (type) {
case GET_FORECAST:
return {
...state,
forecast: payload,
loading: false
}
case FORECAST_ERROR:
return {
...state,
forecast: null,
loading: false
}
default:
return state
}
}
My Node Route
// server/routes/api/weather.js
const express = require('express');
const axios = require('axios');
const chalk = require('chalk');
const router = express.Router();
// ***** route: GET to /api/weather
router.get('/weather', async (req, res) => {
try {
// build url to weather api
const keys = require('../../../client/src/config/keys');
const baseUrl = keys.darkskyBaseUrl;
const apiKey = keys.darkskyApiKey;
const lat = keys.locationLat;
const lng = keys.locationLng;
const url = `${baseUrl}${apiKey}/${lat},${lng}`;
console.log(chalk.blue('SERVER SIDE ROUTE FORECAST URL ', url));
const res = await axios.get(url);
// forecast -- strip down res, only using currently{} & daily{}
const weather = {
currently: res.data.currently,
daily: res.data.daily.data
};
console.log(chalk.yellow('SERVER SIDE ROUTE FORECAST DATA ', weather));
// return weather
res.json({ weather });
} catch (error) {
console.error(chalk.red('ERR ',error.message));
res.status(500).send('Server Error');
}
});
module.exports = router;
My Express server middleware pertaining to routes (just to be thorough)
// server/index.js
/* code deleted for brevity */
// define routes
app.use('/api/users', require('./routes/api/users'));
app.use('/api/auth', require('./routes/api/auth'));
app.use('/api/weather', require('./routes/api/weather'));
app.use('/api/favorites', require('./routes/api/favorites'));
/* code deleted for brevity */
If the code snippets included are not sufficient, the repo resides here: https://github.com/dhawkinson/TH12-BnBConcierge
Thank you in advance for help with this.
***** Updates *****
I notice that the console logs I have in both actions/weather.js & reducers/weather.js on the client side & routes/api/weather.js on the server side are NOT firing. That tells me that those modules must not be executing. That would explain why I am getting the error "Cannot read property 'currently' of undefined" in client/src/components/pages/functional/Weather.js. Clearly I have a missing link in this chain. I just can't see what it is.
I tried a small refactor, based on input below. I was trying to see if there was some kind of naming conflict going on. this is what I did in my React functional Component:
// client/src/components/pages/functional/Weather.js
...
const mapStateToProps = state => ({weather: { forecast: state.forecast, loading: state.loading }});
...
It didn't help.
I see that in your combineReducers here you are setting as
export default combineReducers({
alert,
auth,
weather
})
So in the store, things gets saved as { alert: {...}, auth: {...}, weather: {...}}. Can you try accessing the forecast value in your Weather as state.weather.forecast ?
const mapStateToProps = state => ({ forecast: state.weather.forecast });
Let me know if it works.
You need to modify your component.
const dispatch = useDispatch();
useEffect(() => { dispatch(getWeather()); }, [getWeather])
And your mapToStateToProps should be as follows:
const mapStateToProps = state => ({ forecast: state.weather.forecast });

Losing currentUser when App component refreshes?

I am currently working on a simple web app using React, Node.js, passport-google-oauth20.
For some reason, whenever the application re-mounts, I lose the currentUser state. It's as if req.user is just lost.
The way it works is I first fetch the current user when the application mounts and set this.state.currentUser to the results. The issue is when the page refreshes. The component re-mounts and I lose the user. I tried logging the user and receive null.
Hopefully I explained this well enough... here is the code:
App component:
import React from 'react';
import List from './List';
import ListForm from './ListForm';
import * as helpers from '../helpers';
class App extends React.Component{
state = {
currentUser: '',
}
componentDidMount() {
helpers.fetchUser()
.then(data => {
this.setState({
currentUser: data
});
});
}
onSubmit = (id, item) => {
helpers.addItem(id, item);
}
render() {
return (
<div className="container">
<List
currentUser={this.state.currentUser}
onSubmit={this.onSubmit}
/>
</div>
);
}
};
export default App;
Helpers:
import axios from 'axios';
export const fetchUser = async () => {
const resp = await axios.get('/api/current_user');
return resp.data;
}
export const addItem = (id, newItem) => {
axios.post("/api/" + id + "/addItem", newItem);
}

How to create a 'Load More' feature without re-rendering entire component in React/Node?

I'm trying to create a simple poll app, where you can make new polls.
In the section 'MyPolls', I want it to render only the first 5 polls that I've made instead of rendering the entire list of polls.
At the bottom is a 'Load More' button, where upon clicking, loads another 5 polls and so on.
I've been using Mongoose/MongoDB backend and my approach has been to use skip and limit.
I've managed to implement this feature, but the problem is the entire component re-renders, which is annoying for a user as you have to scroll down again the click the 'Load More' button.
Here is my app: https://voting-app-drhectapus.herokuapp.com/
(use can you these login details for convenience:
username: riverfish#gmail.com
password: 123)
And then goto the My Polls page.
MyPoll.js:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../actions';
class MyPolls extends Component {
constructor(props) {
super(props);
this.state = {
skip: 0
};
}
componentDidMount() {
this.props.fetchMyPolls(this.state.skip);
this.setState({ skip: this.state.skip + 5 });
}
sumVotes(polls) {
return polls.reduce((a, b) => {
return a.votes + b.votes;
});
}
loadMore(skip) {
this.props.fetchMyPolls(skip);
const nextSkip = this.state.skip + 5;
this.setState({ skip: nextSkip });
}
renderPolls() {
return this.props.polls.map(poll => {
return (
<div className='card' key={poll._id}>
<div className='card-content'>
<span className='card-title'>{poll.title}</span>
<p>Votes: {this.sumVotes(poll.options)}</p>
</div>
</div>
)
})
}
render() {
console.log('polls', this.props.polls);
console.log('skip:', this.state.skip);
return (
<div>
<h2>My Polls</h2>
{this.renderPolls()}
<a href='#' onClick={() => this.loadMore(this.state.skip)}>Load More</a>
</div>
);
}
}
function mapStateToProps({ polls }) {
return { polls }
}
export default connect(mapStateToProps, actions)(MyPolls);
Action creator:
export const fetchMyPolls = (skip) => async dispatch => {
const res = await axios.get(`/api/mypolls/${skip}`);
dispatch({ type: FETCH_MY_POLLS, payload: res.data });
}
Poll route:
app.get('/api/mypolls/:skip', requireLogin, (req, res) => {
console.log(req.params.skip);
Poll.find({ _user: req.user.id })
.sort({ dateCreated: -1 })
.skip(parseInt(req.params.skip))
.limit(5)
.then(polls => {
res.send(polls);
});
});
Entire github repo: https://github.com/drhectapus/voting-app
I understand that might method of implementing this feature might be the best possible solution so I'm open to any suggestions.
It looks like the re-render is triggered by the fact that clicking the "Load More" link actually causes react router to navigate to a new route, causing the entire MyPolls component to re-render.
Just replace the <a href='#' onClick={...}> with <button onClick={...}>.
If you don't want to use a button, you could also change the onClick function to
const onLoadMoreClick = e => {
e.preventDefault(); // this prevents the navigation normally occuring with an <a> element
this.loadMore(this.state.skip);
}

Resources