React Redux useEffect not loading data on initial page render - redux

I'm trying to render a quiz object from an api and am using useEffect to do so, but it doesn't seem to be working.
Here is my Quiz component:
export function Quiz(props) {
const {
quiz,
fetchQuiz,
} = props;
useEffect(() => {
fetchQuiz();
}, [])
return (
<div id="wrapper">
{ quiz.question }
</div>
)
}
const mapStateToProps = state => {
return {
quiz: state.quiz,
}
}
export default connect(mapStateToProps, {
fetchQuiz,
})(Quiz)
my action creator:
export function fetchQuiz() {
return function (dispatch) {
axios. get('http://localhost:9000/api/quiz/next')
.then(res => {
dispatch({ type: SET_QUIZ_INTO_STATE, payload: res.data })
})
.catch(err => console.error(err))
}
}
my reducer:
const initialQuizState = null
function quiz(state = initialQuizState, action) {
switch(action.type) {
case SET_QUIZ_INTO_STATE:
return action.payload
default:
return state
}
}
So when I console.log "quiz" in my Quiz component, I get the object that I am expecting and it's all good. But when I try and call "quiz.question" I get the error: Cannot read properties of null (reading question). Which leads me to believe useEffect is not loading the data from the api correctly. Am I doing something else wrong?

When accessing an object from an API response in the DOM, make sure you have a conditional to check for completion like so:
return (
<div id="wrapper">
{ quiz ? quiz.question : 'No Quiz Here' }
</div>
)
}

Related

React component does not update after Redux store changes

I am trying to fetch some data in a react component using the useEffect hook. After the initial render, fetchItems() gets the items and updates the store. However, items is still an empty object even after the store updates.
I might be using useEffects wrong. How do you use Redux with useEffects? I want to set a loading state for the component, but since the component only dispatches an action to fetch items (instead of directly calling the API), it does not know when the data is fetched and the store is updated so it can pull it.
Can someone please help figure out how to make sure that items object is updated after the saga fetch and the subsequent store update?
import React, { useState, useEffect } from "react";
import { connect } from 'react-redux';
import { useParams } from "react-router-dom";
const ItemComponent = ({ item, fetchItem }) => {
const { itemId } = useParams();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true)
fetchItem(itemId)
setIsLoading(false)
}, []);
console.log(item) // gives empty object even after the fetch and store update
}
const mapStateToProps = (state) => {
return {
item: state.item
}
}
const mapDispatchToProps = (dispatch) => {
return {
fetchItem: (itemId) => { dispatch(fetchItemActionCreator(itemId)) }
}
}
export default connect(mapStateToProps, mapDispatchToProps)(ItemComponent);
fetchItemActionCreator is an action creator that creates the action to be dispatched.
My reducer and saga work fine as I can see the store actions and updates in the console.
If I pass the items object into the dependency array for useEffect, then there will be an infinite loop and the page keeps re-rendering.
Reducer:
const itemReducer = (state={}, { type, payload }) => {
switch(type) {
case ITEM_GET_SUCCESS:
return {...state, ...payload}
default: return state
}
}
fetchItemActionCreator:
import { createAction } from '#reduxjs/toolkit';
export const fetchItemActionCreator = createAction(ITEM_GET_PENDING);
Thank you very much in advance!
I want to set a loading state for the component
/** Action */
const getItem = () => dispatch => {
dispatch({ type: 'GET_ITEM_START' });
axios
.get('your api end point')
.then(res => {
const item = res.data;
dispatch({
type: 'GET_ITEM_SUCCESS',
payload: {
item,
},
});
})
.catch(error => {
dispatch({
type: 'GET_ITEM_FAIL',
payload: error,
});
});
};
/** Reducer */
const INITIAL_STATE = {
item: null,
error: '',
loading: false,
};
const itemReducer = (state = INITIAL_STATE, { type, payload }) => {
switch (type) {
case 'GET_ITEM_START':
return { ...state, error: '', loading: true };
case 'GET_ITEM_SUCCESS':
return { ...state, ...payload, loading: false };
case 'GET_ITEM_FAIL':
return { ...state, error: payload, loading: false };
default:
return state;
}
};
Then your could handle Loading state in your component
const ItemComponent = ({ fetchItem, item, loading, error }) => {
/** ... */
/**
Check for loading and show a spinner or anything like that
*/
useEffect(() => {
fetchItem(itemId);
}, []);
if (loading) return <ActivityIndicator />;
if (item) return <View>{/* renderItem */}</View>;
return null;
};

Why, while using useEffect() and .then() in Redux, I get an Error: Actions must be plain objects. Use custom middleware for async actions

using Redux and am now straggling with a signin and signout button while using oauth.
When I press on the button to logIn, the popup window appears and I can choose an account. But in the meantime the webpage throws an error.
I got the following error as stated in the title:
Error: Actions must be plain objects. Use custom middleware for async actions.
I am using hooks, in this case useEffect().then() to fetch the data.
1) Why?
2) Also do not know, why I am getting a warning: The 'onAuthChange' function makes the dependencies of useEffect Hook (at line 35) change on every render. Move it inside the useEffect callback. Alternatively, wrap the 'onAuthChange' definition into its own useCallback() Hook react-hooks/exhaustive-deps
Here is my code:
GoogleAuth.js
import React, { useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { signIn, signOut } from "../actions";
const API_KEY = process.env.REACT_APP_API_KEY;
const GoogleAuth = () => {
const isSignedIn = useSelector((state) => state.auth.isSignedIn);
console.log("IsSignedIn useSelector: " + isSignedIn);
const dispatch = useDispatch();
const onAuthChange = () => {
if (isSignedIn) {
dispatch(signIn());
} else {
dispatch(signOut());
}
};
useEffect(
() => {
window.gapi.load("client:auth2", () => {
window.gapi.client
.init({
clientId: API_KEY,
scope: "email"
})
.then(() => {
onAuthChange(window.gapi.auth2.getAuthInstance().isSignedIn.get());
console.log("isSignedIn.get(): " + window.gapi.auth2.getAuthInstance().isSignedIn.get());
window.gapi.auth2.getAuthInstance().isSignedIn.listen(onAuthChange);
});
});
},
[ onAuthChange ]
);
const onSignInOnClick = () => {
dispatch(window.gapi.auth2.getAuthInstance().signIn());
};
const onSignOutOnClick = () => {
dispatch(window.gapi.auth2.getAuthInstance().signOut());
};
const renderAuthButton = () => {
if (isSignedIn === null) {
return null;
} else if (isSignedIn) {
return (
<button onClick={onSignOutOnClick} className="ui red google button">
<i className="google icon" />
Sign Out
</button>
);
} else {
return (
<button onClick={onSignInOnClick} className="ui red google button">
<i className="google icon" />
Sign In with Google
</button>
);
}
};
return <div>{renderAuthButton()}</div>;
};
export default GoogleAuth;
reducer/index.js
import { combineReducers } from "redux";
import authReducer from "./authReducer";
export default combineReducers({
auth: authReducer
});
reducers/authReducer.js
import { SIGN_IN, SIGN_OUT } from "../actions/types";
const INITIAL_STATE = {
isSignedIn: null
};
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case SIGN_IN:
return { ...state, isSignedIn: true };
case SIGN_OUT:
return { ...state, isSignedIn: false };
default:
return state;
}
};
actions/index.js
import { SIGN_IN, SIGN_OUT } from "./types";
export const signIn = () => {
return {
type: SIGN_IN
};
};
export const signOut = () => {
return {
type: SIGN_OUT
};
};
types.js
export const SIGN_IN = "SIGN_IN";
export const SIGN_OUT = "SIGN_OUT";
The reason of the first error is that, inside both onSignInOnClick and onSignInOnClick, dispatch() receives a Promise (since window.gapi.auth2.getAuthInstance().signIn() returns a Promise).
There are different solution to handle effects in redux, the simplest are redux promise or redux thunk.
Otherwise you can dispatch the { type: SIGN_IN } action, and write a custom middleware to handle it.
The reason of the second error, is that the onAuthChange is redefined on every render, as you can see here:
const f = () => () => 42
f() === f() // output: false
Here's a possible solution to fix the warning:
useEffect(() => {
const onAuthChange = () => {
if (isSignedIn) {
dispatch(signIn())
} else {
dispatch(signOut())
}
}
window.gapi.load('client:auth2', () => {
window.gapi.client
.init({
clientId: API_KEY,
scope: 'email',
})
.then(() => {
onAuthChange(window.gapi.auth2.getAuthInstance().isSignedIn.get())
console.log(
'isSignedIn.get(): ' +
window.gapi.auth2.getAuthInstance().isSignedIn.get(),
)
window.gapi.auth2.getAuthInstance().isSignedIn.listen(onAuthChange)
})
})
}, [isSignedIn])

Next.js + Redux server side rendering: Has data, but doesn't render on server side

I'm trying to add redux integration to my Next.js app, but I can't get serverside rendering working the way it should. I based my implementation off the official nextjs redux example.
In the end, when the page comes back from the server, the data is present as JSON data in the output, but the actual rendering based on this data did not happen. The weird thing is that before I used redux, the content DID render the way it should.
Naturally, I'm also getting React's checksum warning, indicating that the markup on the server is different.
I have no idea how to make this work properly on the server side. Is there something that I'm missing?
Here's the HTML generated by Next.js:
<h1 data-reactid="3">Test page</h1>
</div></div></div><div id="__next-error"></div></div><div><script>
__NEXT_DATA__ = {"props":{"isServer":true,"store":{},
"initialState":{"authors":{"loading":false,"items":{"4nRpnr66B2CcQ4wsY04CIQ":… }
,"initialProps":{}},"pathname":"/test","query":{},"buildId":1504364251326,"buildStats":null,"assetPrefix":"","nextExport":false,"err":null,"chunks":[]}
module={}
__NEXT_LOADED_PAGES__ = []
__NEXT_LOADED_CHUNKS__ = []
__NEXT_REGISTER_PAGE = function (route, fn) {
__NEXT_LOADED_PAGES__.push({ route: route, fn: fn })
}
__NEXT_REGISTER_CHUNK = function (chunkName, fn) {
__NEXT_LOADED_CHUNKS__.push({ chunkName: chunkName, fn: fn })
}
</script><script async="" id="__NEXT_PAGE__/test" type="text/javascript" src="/_next/1504364251326/page/test"></script><script async="" id="__NEXT_PAGE__/_error" type="text/javascript" src="/_next/1504364251326/page/_error/index.js"></script><div></div><script type="text/javascript" src="/_next/1504364251326/manifest.js"></script><script type="text/javascript" src="/_next/1504364251326/commons.js"></script><script type="text/javascript" src="/_next/1504364251326/main.js"></script></div></body></html>
AS you can see, the initialState value is populated, it contains all the required data, but the DOM still shows empty!.
If I render the dom on the client side, the js picks up the initial content and rerenders the page with the loaded content in place.
Here's my test page JS file:
import React from 'react'
import map from 'lodash.map';
import { initStore } from '../lib/store';
import * as actions from '../lib/actions';
import withRedux from 'next-redux-wrapper';
class IndexPage extends React.PureComponent {
static getInitialProps = ({ store, req }) => Promise.all([
store.dispatch(actions.fetchAll)
]).then( () => ({}) )
render() {
const latestPlants = this.props.plants.latest || [];
return (
<div>
<h1>Test page</h1>
{ map(this.props.plants.items, p => (
<div>{p.fields.name}</div>
))}
</div>
)
}
}
export default withRedux(initStore, data => data, null)(IndexPage)
For whatever it's worth, here's the action that I call above:
export const fetchAll = dispatch => {
dispatch({
type: LOADING_ALL
})
return axios.get('/api/frontpage')
.then( response => {
const data = response.data
dispatch({
type: RESET_AUTHORS,
payload: data.authors
})
dispatch({
type: RESET_PLANTS,
payload: data.plants
})
dispatch({
type: RESET_POSTS,
payload: data.posts
})
});
}
Any help with this would be greatly appreciated, I'm at a loss on how to make this work as expected. Anyone have any leads? Please also comment if there's something I can clarify.
I recommend to split the code in different parts. First, I'll create a store, with something like this:
import { createStore, applyMiddleware } from 'redux';
import thunkMiddleware from 'redux-thunk';
import reducer from './reducers'
export const initStore = (initialState = {}) => {
return createStore(reducer, initialState, applyMiddleware(thunkMiddleware))
}
Then I'll create the store with the types to handle:
const initialState = {
authors: null,
plants: null,
posts: null
}
export default (state = initialState, action) => {
switch (action.type) {
case 'RESET':
return Object.assign({}, state, {
authors: action.authors,
plants: action.plants,
posts: action.posts
})
default:
return state
}
}
In the actions I'll have something like this:
export const fetchAll = dispatch => {
return axios.get('/api/frontpage')
.then( response => {
const data = response.data
dispatch({
type: 'RESET',
authors: data.authors,
plants: data.plants,
posts: data.posts
})
});
}
The index will be something like this:
import React from 'react'
import { initStore } from '../store'
import withRedux from 'next-redux-wrapper'
import Main from '../components'
class Example extends React.Component {
render() {
return (
<div>
<Main />
</div>
)
}
}
export default withRedux(initStore, null)(Example)
And the component Main:
import React, {Component} from 'react'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { fetchAll } from '../../actions'
class Data extends Component {
componentWillMount() {
this.props.fetchAll()
}
render() {
const { state } = this.props
return (
<div>
<h1>Test page</h1>
{ map(state.plants.items, p => (
<div>{p.fields.name}</div>
))}
</div>
)
}
}
const mapStateToProps = (state) => {
return {
state
}
}
const mapDistpatchToProps = dispatch => {
return {
fetchAll: bindActionCreators(fetchAll, dispatch)
}
}
export default connect(mapStateToProps, mapDistpatchToProps)(Data)
Make the changes for what you need.
You can check some full examples here:
Form handler
Server Auth

Redux - Passing reducer an action seems to have undefined id

I did the basic redux todolist tutorial and it worked but I wanted to get to know the code by making a small change.
I changed:
actions/index.js
let nextTodoId = 0
export const addTodo = (text) => {
return {
type: 'ADD_TODO',
id: nextTodoId++,
text
}
}
to this:
let nextTodoId = 0
export const addTodo = (text) => {
return {
type: 'ADD_TODO',
payload: {
id: nextTodoId++,
text: text
}
}
}
And I got the adding a todo working with that but a strange side effect has occurred in the toggleTodo - There are no console errors but clicking a todo list item is supposed to toggle it between being completed (visually has a strike through the text) and being not completed. Clicking a list item now has no effect.
I'm struggling to pass this reducer an action which has a defined id.
reducers/todos.js:
This is the code which calls the toggleTodo(id) reducer (look for the arrow pointing and saying "HERE"):
containers/visibleTodoList.js:
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const mapDispatchToProps = (dispatch) => {
return {
onTodoClick: (id) => {
dispatch(toggleTodo(id)) <-------------HERE
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
So the id is coming from onTodoClick.
components/TodoList.js:
So I pause it there and see the value of onTodoClick():
Where in the original code was it setting onTodoClick.id so I can repeat it again to get the onTodoClick.id to be defined (thus hopefully causing the clicking a todo item to toggle successfully).
You are looking for state.id !== action.id but you pass it as action.payload.id do the following:
case 'TOGGLE_TODO':
if (state.id !== action.payload.id) {
return state
}

React props using Meteor Apollo

I am playing with the Meteor Apollo demo repo.
I am having difficulty passing variables down to children with React. I am getting an error
imports/ui/Container.jsx:10:6: Unexpected token (10:6)
The below code is the Container.jsx component:
import React from 'react';
import { Accounts } from 'meteor/std:accounts-ui';
class Container extends React.Component {
render() {
let userId = this.props.userId;
let currentUser = this.props.currentUser;
}
return (
<Accounts.ui.LoginForm />
{ userId ? (
<div>
<pre>{JSON.stringify(currentUser, null, 2)}</pre>
<button onClick={() => currentUser.refetch()}>Refetch!</button>
</div>
) : 'Please log in!' }
);
}
}
It is passed props via the Meteor Apollo data system (I have omitted some imports at the top):
const App = ({ userId, currentUser }) => {
return (
<div>
<Sidebar />
<Header />
<Container userId={userId} currentUser={currentUser} />
</div>
)
}
// This container brings in Apollo GraphQL data
const AppWithData = connect({
mapQueriesToProps({ ownProps }) {
if (ownProps.userId) {
return {
currentUser: {
query: `
query getUserData ($id: String!) {
user(id: $id) {
emails {
address
verified
}
randomString
}
}
`,
variables: {
id: ownProps.userId,
},
},
};
}
},
})(App);
// This container brings in Tracker-enabled Meteor data
const AppWithUserId = createContainer(() => {
return {
userId: Meteor.userId(),
};
}, AppWithData);
export default AppWithUserId;
I would really appreciate some pointers.
I believe the error is that you accidentally ended the render function before the return statement.
render() { // <- here it starts
let userId = this.props.userId;
let currentUser = this.props.currentUser;
} // <- here it ends
Another error is that your return statement doesn't return a single DOM element, but two of them: an Accounts.ui.LoginForm and a div. The return function should only return one element. Just put the entire thing into a single <div>.

Resources