Redux Store not populated before Component Render - reactjs

I am working on the authentification procedure for an app I'm developing.
Currently, the user logins in through Steam. Once the login is validated the server redirects the user to the app index, /, and issues them a pair of JWTs as GET variables. The app then stores these in a Redux store before rewriting the URL to hide the JWT tokens for security purposes.
The app then decodes the tokens to obtain info about the user, such as their username and avatar address. This should be rendered in the app's SiteWrapper component, however, this is where my problem occurs.
What seems to be happening is SiteWrapper component loads before the App component finishes saving the tokens and thus throws errors as variables are not defined. Most of the fixes that seem relevant are for API requests, however, in this case, that is not the case. I already have the data in the URL. I'm not sure if the same applies.
Any suggestions on how to fix this? Any other best practice advice would be appreciated. I'm new to both React and Redux.
Error
Index
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { Provider } from 'react-redux';
import store from './redux/store';
// console debug setup
window.store = store;
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'));
serviceWorker.unregister();
App
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import { connect } from 'react-redux';
import "tabler-react/dist/Tabler.css";
import history from './utils/history';
import {
storeRefreshJWTToken,
storeAccessJWTToken,
loadUserFromJWTRefreshToken
} from "./redux/app";
import {
HomePage
} from './pages';
class App extends Component {
componentDidMount() {
//Get tokens from URL when app loads and then hide them from url.
const urlParams = new URLSearchParams(window.location.search);
if(urlParams.has('access_token') && urlParams.has('refresh_token')){
this.props.storeRefreshJWTToken(urlParams.get('refresh_token'));
this.props.storeAccessJWTToken(urlParams.get('access_token'));
//Load user info from obtained tokens.
this.props.loadUserFromJWTRefreshToken();
}
history.push('/');
}
render() {
return (
<React.StrictMode>
<Router basename={process.env.PUBLIC_URL} history={history}>
<Switch>
<Route exact path="/" component={HomePage}/>
</Switch>
</Router>
</React.StrictMode>
);
}
}
const mapStateToProps = (state) => ({});
const mapDispatchToProps = {
storeRefreshJWTToken,
storeAccessJWTToken,
loadUserFromJWTRefreshToken
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
SiteWrapper
import * as React from "react";
import { withRouter } from "react-router-dom";
import { connect } from 'react-redux';
import {
Site,
Nav,
Grid,
List,
Button,
RouterContextProvider,
} from "tabler-react";
class SiteWrapper extends React.Component {
componentDidMount() {
console.log(this.props);
this.accountDropdownProps = {
avatarURL: this.props.user.avatar,
name: this.props.user.display_name,
description: "temp",
options: [
{icon: "user", value: "Profile"},
{icon: "settings", value: "Settings"},
{isDivider: true},
{icon: "log-out", value: "Sign out"},
],
};
}
render(){
return (
<Site.Wrapper
headerProps={{
href: "/",
alt: "Tabler React",
imageURL: "./demo/brand/tabler.svg",
navItems: (
<Nav.Item type="div" className="d-none d-md-flex">
<Button
href="https://github.com/tabler/tabler-react"
target="_blank"
outline
size="sm"
RootComponent="a"
color="primary"
>
Source code
</Button>
</Nav.Item>
),
accountDropdown: this.accountDropdownProps,
}}
navProps={{ itemsObjects: this.props.NavBarLinks }}
routerContextComponentType={withRouter(RouterContextProvider)}
footerProps={{
copyright: (
<React.Fragment>
Copyright © 2018
Thomas Smyth.
All rights reserved.
</React.Fragment>
),
nav: (
<React.Fragment>
<Grid.Col auto={true}>
<List className="list-inline list-inline-dots mb-0">
<List.Item className="list-inline-item">
Developers
</List.Item>
<List.Item className="list-inline-item">
FAQ
</List.Item>
</List>
</Grid.Col>
</React.Fragment>
),
}}
>
{this.props.children}
</Site.Wrapper>
);
}
}
const mapStateToProps = (state) => {
console.log(state);
return {
user: state.App.user
};
};
export default connect(mapStateToProps)(SiteWrapper);
Reducer
import initialState from './initialState';
import jwt_decode from 'jwt-decode';
//JWT Auth
const STORE_JWT_REFRESH_TOKEN = "STORE_JWT_REFRESH_TOKEN";
export const storeRefreshJWTToken = (token) => ({type: STORE_JWT_REFRESH_TOKEN, refresh_token: token});
const STORE_JWT_ACCESS_TOKEN = "STORE_JWT_ACCESS_TOKEN";
export const storeAccessJWTToken = (token) => ({type: STORE_JWT_ACCESS_TOKEN, access_token: token});
// User
const LOAD_USER_FROM_JWT_REFRESH_TOKEN = "DEC0DE_JWT_REFRESH_TOKEN";
export const loadUserFromJWTRefreshToken = () => ({type: LOAD_USER_FROM_JWT_REFRESH_TOKEN});
export default function reducer(state = initialState.app, action) {
switch (action.type) {
case STORE_JWT_REFRESH_TOKEN:
return {
...state,
jwtAuth: {
...state.jwtAuth,
refresh_token: action.refresh_token
}
};
case STORE_JWT_ACCESS_TOKEN:
return {
...state,
JWTAuth: {
...state.jwtAuth,
access_token: action.access_token
}
};
case LOAD_USER_FROM_JWT_REFRESH_TOKEN:
const user = jwt_decode(state.jwtAuth.refresh_token);
return {
...state,
user: user
};
default:
return state;
}
}
Store
import { createStore, combineReducers, applyMiddleware } from 'redux';
import ReduxThunk from 'redux-thunk'
import App from './app';
const combinedReducers = combineReducers({
App
});
const store = createStore(combinedReducers, applyMiddleware(ReduxThunk));
export default store;

componentDidMount() is going to run after first render. So on first render the this.props.user inside will be null at that point.
You could:
move the async call to componentWillMount() (not recommended)
put a guard in the SiteWrapper() so it can handle null case
not render Home from the App until the async call has finished

in componentDidMount() you are expecting user to be set.
You can:
Set user in initial state with some "isAuth" flag that you can easily check
As componentWillMount() is deprecated you can use componentDidUpdate() to check if user state is changed and do some actions.
Use asyns functions when sawing YWT

You could of course check for null and don't try to show the user's details in your SiteWrapper's componentDidMount()-method. But why? Do you have an alternative route to go if your user couldn't be found and is null? I guess no. So you basically have two options:
The ideal solution is to implement async actions and show an activity
indicator (e.g. spinner) until the jwt-token is loaded. Afterwards
you can extract your user information and fully render your
component as soon as the fetch is succesfully completed.
If you can't use async action, for whatever reason, I would suggest
the "avoid-null" approach. Put a default user in your initial
state and it should be done. If you update the user prop, the
component will rerender anyways (if connected properly).

I have solved my issue. It seems this was one of those rare cases where trying to keep things simple and develop was bad.
Now, when my application loads I either show the user information or a login button.
Once the key is loaded from the URL the component rerenders to show the user information.
This does increase the number of renders, however, it is probably the best way of doing it.

Related

React SSR React Router Dom Switch, Route, Link "Invariant Failed"

I beleive the proper discription of this issue is explained here by timdorr. I tried exporting App.js from the bundle but get window undefined errors. SO still stuck
SSR/Client React Router Dom "Switch" breaks for me with a "Invariant Failed". I believe it says it has something to do with Switch not been allowed outside "Router", which it is inside.
The minimal project link is below, that may be easier way look at the project. I have listed the main files below
1: SERVER SIDE RENDER ENDPOINT
// EXPRESS ROUTER
const express = require("express");
const aRouter = express.Router();
// REACT UTILITIES
import React from "react";
import { renderToNodeStream } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import { createMemoryHistory } from "history";
import Loadable from "react-loadable";
// REDUX UTILITIES
import { init } from "../src/module/store";
import { Provider } from "react-redux";
// CUSTOM COMPONENTS
import App from "../src/App";
// UTILITIES
import fs from "fs-extra";
import renderUtils from "../utils/renderUtils";
// ASSETS
import { initState } from "../assets/store/init";
aRouter.get(["/", "/home"], async function (req, res) {
console.log("RENDER HOME");
try {
// INITIAL WRITE TO CLIENT - START HEAD
res.setHeader('Content-Type', 'text/html');
res.write("<!DOCTYPE html>");
res.write("<html style='scrollbar-width: none;'>");
let headHTML = await fs.readFile("./public/head.html", "utf-8");
let scriptsHTML = await fs.readFile("./public/scripts.html", "utf-8");
res.write(headHTML);
res.write("<body>");
res.write(`<div id = "app-container">`);
// INITALISING STATE
var initialState = initState();
initialState.article.articles = {
"abcde": {
title: "My First Article",
body: "This is my first article"
},
"fghij": {
title: "My Second Article",
body: "This is my second article"
},
"klmno": {
title: "My Third Article",
body: "This is my third article"
}
};
initialState.article.fetched = true;
initialState.ui.user = { type: "" };
initialState.ui.global = {
team: "Arsenal",
teamID: 19
};
const history = createMemoryHistory({ initialEntries: [req.originalUrl] });
const store = init(history, initialState);
// THE ISSUE SEEMS TO BE TO DO WITH THIS SERVER SIDE STATIC BROWSER AND THE CLIENT BORWSER ROUTER
const stream = renderToNodeStream(
<Provider store = {store}>
<StaticRouter history = {history} location = {req.originalUrl} context = {{}}>
<App />
</StaticRouter>
</Provider>
);
stream.pipe(res, { end: false });
stream.on("end", renderUtils.onRenderEnd.bind(this, res, store, scriptsHTML));
} catch (err) { renderUtils.onRenderError.bind(this, res, "RENDER HOME ERROR", err.message); }
});
var self = (module.exports = aRouter);
2: CLIENT INDEX
// REACT
import React from "react";
import ReactDOM from "react-dom";
import Loadable from "react-loadable";
import { BrowserRouter } from "react-router-dom";
import { createMemoryHistory } from "history";
import { Provider } from "react-redux";
// REDUX
import { init } from "./module/store";
// CREATE STORE
let history = createMemoryHistory();
let store = init(history, window.INITIAL_STATE);
// MAIN APP COMPONENT
import App from "./App";
// MOUNTED STYLES
import "./style/client/index.scss";
const renderApp = () => {
ReactDOM.hydrate(
<Provider store = {store}>
<BrowserRouter history = {history}>
<App />
</BrowserRouter>
</Provider>,
document.getElementById("app-container")
);
};
store.subscribe(() => renderApp());
3: APP - CLIENT
// REACT
import React, { PureComponent } from "react";
import { Switch, Route } from "react-router-dom";
import PropTypes from "prop-types";
// REDUX STORE
import { connect } from "react-redux";
import { getName, getAge, getPosition } from "./module/user/userReducer";
import { getUIElement, setUIElement } from "./module/uiReducer";
// IMPORT CUSTOM COMPONENTS
import Routes from "./Routes";
class App extends PureComponent {
componentDidMount = () => this.props.setUI("user", "type", "admin");
render = () => {
return (
<div className = "app">
<span>My App</span>
<span>Name : {this.props.name}</span>
<span>Age : {this.props.age}</span>
<span>Position : {this.props.position}</span>
<span>Team : {this.props.team}</span>
<span>Team ID : {this.props.teamID}</span>
<span>Type : {this.props.type}</span>
<div>
<Switch>
<Route path = "/" component = {MyLocation} />
<Route path = "/contact" render = {() => (<MyLocation location = "Contact" />)} />
</Switch>
</div>
</div>
);
};
};
App.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number.isRequired,
position: PropTypes.string.isRequired,
team: PropTypes.string.isRequired,
teamID: PropTypes.number.isRequired,
type: PropTypes.string.isRequired,
setUI: PropTypes.func.isRequired
};
const mapStateToProps = state => ({
name: getName(state),
age: getAge(state),
position: getPosition(state),
team: getUIElement(state, "global", "team"),
teamID: getUIElement(state, "global", "teamID"),
type: getUIElement(state, "user", "type")
});
const mapDispatchToProps = dispatch => ({
setUI: (component, element, value) => dispatch(setUIElement({ component, element, value }))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);
The full minimal react app here
It just breaks when I add the Switch and Routes. The Switch "IS INSIDE" the Browser Router. I have read articles which explain to send the same React Router Dom to the client, but I tried them explanations and they dont work for me.
To run the project simply run "yarn", "npm run build" and "npm start". The app has just one page with some filler text
Issue : Breaks at Switch
Required: Work at Switch
Tried: Explantions that explain to bring same instance of react-router-dom to client from server and use webpack alies etc.
Timdorr (Shared at start of question) explains this.
There is a new React.createContext API that we use in 5.0 to replace the legacy context usage. This involves creating a "context" pair, a set of Provider and Consumer components. We create that in a singleton module that is imported into Router and Link directly. In this new API, you have to use the exact same instance. Two separately-created contexts will never match up and won't be visible to each other.
Whats also funny is this works live on Heroku "production", but doesnt work locally "production". Im thinking heroku have some fallback code catching it.
Any help be great;
Daniel
After a lot of playing around I discovered the issue stemmed from
import { withRouter } from "react-router-dom";
export default withRouter(Component);
I think withRouter doesn't exists anymore on this new version and as as to why it worked with Heroku is a mystery. I think Heroku has great version controlling and debugging/handling.
I started using the hook useHistory instead and converted my classic components to function component with hooks and all is well now
Daniel

How do I stop state store data from accumulating in a redux component every time I navigate to it using react router

Okay, caveat is that I'm very very new to redux. I'm doing a course on it atm and I'm trying to step outside the box a little and generate a fairly standard website using the wordpress API and Redux. I appreciate that redux is generally meant for larger things but this seems like a useful first step in the learning process.
I have a series of components which list out posts, pages and different types of custom posts taken from the wordpress API and I navigate between these using react-router-dom.
The problem is that every time I go back to a component/view the list of posts or pages is rendered again so, for example, the first time I go there the list might be: test post 1, test post 2, the second time it would be: test post 1, test post 2, test post 1, test post 2, the third time: test post 1, test post 2, test post 1, test post 2, test post 1, test post 2 etc etc etc.
The reason for this is obvious, each time the component is rendered the data gets pulled from the store and rendered, however, as the entire app doesn't rerender as it would be with plain old reactjs, it doesn't cleared.
My question, of course is what's the best way of going about fixing this. I've read some kind of related posts which advise attaching some kind of condition to the component to check whether the data is already present but I've no idea how to do this and can't find out how. My attempts haven't worked because it seems that any var returned from componentDidMount is not seen in the render method.
Thanks in advance.
Code is below:
src/index.js
import React from "react";
import { BrowserRouter as Router } from 'react-router-dom';
import { render } from "react-dom";
import { Provider } from "react-redux";
import store from "./js/store/index";
import App from "./js/components/App";
render(
<Router>
<Provider store={store}>
<App />
</Provider>
</Router>,
document.getElementById("root")
);
src/js/index.js
import store from "../js/store/index";
window.store = store;
src/js/store/index.js
import { createStore, applyMiddleware, compose } from "redux";
import rootReducer from "../reducers/index";
import thunk from "redux-thunk";
const storeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
rootReducer,
storeEnhancers(applyMiddleware(thunk))
);
export default store;
src/js/reducers/index.js
import { POSTS_LOADED } from "../constants/action-types";
import { PAGES_LOADED } from "../constants/action-types";
const initialState = {
posts: [],
pages: [],
banner_slides: [],
portfolio_items: []
};
function rootReducer(state = initialState, action) {
switch (action.type) {
case 'POSTS_LOADED':
return Object.assign({}, state, {
posts: state.posts.concat(action.payload)
});
case 'PAGES_LOADED':
return Object.assign({}, state, {
pages: state.pages.concat(action.payload)
});
default:
return state;
}
}
export default rootReducer;
src/js/actions/index.js
export function getWordpress(endpoint) {
return function(dispatch) {
return fetch("http://localhost/all_projects/react-wpapi/my_portfolio_site/wordpress/wp-json/wp/v2/" + endpoint )
.then(response => response.json())
.then(json => {
dispatch({ type: endpoint.toUpperCase() + "_LOADED", payload: json });
});
};
}
src/js/constants/action-types.js
export const ADD_ARTICLE = "ADD_ARTICLE";
export const POSTS_LOADED = "POSTS_LOADED";
export const PAGES_LOADED = "PAGES_LOADED";
src/js/components/app.js
import React from "react";
import { Route, Switch, Redirect } from 'react-router-dom';
import Header from "./Header/Header";
import Posts from "./Posts";
import Pages from "./Pages";
import BannerSlides from "./BannerSlides";
import PortfolioItems from "./PortfolioItems";
const App = () => (
<div>
<Header />
<Route render = {({ location }) => (
<Switch location={location}>
<Route
exact path="/posts"
component={Posts}
/>
<Route
exact path="/pages"
component={Pages}
/>
</Switch>
)} />
</div>
);
export default App;
src/js/components/Posts.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { getWordpress } from "../actions/index";
export class Posts extends Component {
componentDidMount() {
this.props.getWordpress('posts');
let test = 1;
return test;
}
render() {
console.log("test: ", test); // not defined
if (test !== 1) {
return (
<ul>
{this.props.posts.map(item => (
<li key={item.id}>{item.title.rendered}</li>
))}
</ul>
);
}
}
}
function mapStateToProps(state) {
return {
posts: state.posts.slice(0, 10)
};
}
export default connect(
mapStateToProps,
{ getWordpress }
)(Posts);
The problem was that, every time you were fetching data, you were adding it to previous data in the array. That's why it was duplicating over time. Just assign instead of adding it in your reducer
function rootReducer(state = initialState, action) {
switch (action.type) {
case 'POSTS_LOADED':
return {
...state,
posts: action.payload
};
case 'PAGES_LOADED':
return {
...state,
pages: action.payload
};
default:
return state;
}
}
Hope it helps :)
If I'm understanding, you want to only fetch initial posts on first mount instead of every time the component is mounted?
In src/js/components/Posts.js you can check if any posts are stored in Redux before fetching inside the CDM lifecycle method. Eg.
componentDidMount() {
// access props.posts which you set inside mapDispatchToProps
if (this.props.posts.length === 0) {
this.props.getWordpress('posts');
}
}
If you are okay with duplicate API calls on every mount, and you are ok with fetching all the posts at once, you can adjust your reducer to overwrite the posts array instead of concat. But overwriting it assumes you want to load all the posts in 1 API call, instead of loading say 25 posts per page or having a 'Load more posts' button.
You need to check your state before calling fetch. I like to put mst of my logic in the redux part of the application (fat action creators) and use my react components only for rendering the current state. I would recommend something like this:
export function getWordpress(endpoint) {
return function(dispatch, getState) {
const currentState = getState();
if (currentState.posts && currentState.posts.length) {
// already initialized, can just return current state
return currentState.posts;
}
return fetch("http://localhost/all_projects/react-wpapi/my_portfolio_site/wordpress/wp-json/wp/v2/" + endpoint )
.then(response => response.json())
.then(json => {
dispatch({ type: endpoint.toUpperCase() + "_LOADED", payload: json });
});
};
}
Later you could separate the logic if posts are initialized into a selector and add some additional layers (like if posts are stale). This way your 'business' logic is easily testabale and separate from your UI.
Hope this helps :)

In React, Firebase auth user is null every time I refresh the page

I got stuck with the below issue. I went through couple of SO questions and answers related to the same issue but none them working for me.
I am using firebase.auth().onAuthStateChanged to check if user is already logged in but I am always getting user null whenever I refresh the page.
PFB code
import React, { Component } from 'react';
import Header from './Header';
import Home from './Home/components/Home';
import Footer from './Footer/components/Footer';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { BrowserRouter as Router, Route, Redirect, withRouter, Switch } from 'react-router-dom';
import { auth } from '../firebase/auth';
import Dashboard from './Dashboard/components/Dashboard';
import * as actions from '../actions/index';
import {Map, List, fromJS} from 'immutable';
class App extends Component {
constructor(props){
super(props);
this.state = {
loading: true,
authenticated: false,
user: null
}
}
componentDidMount() {
auth.onAuthStateChanged(user => {
//user is null every time I refresh the page
if (user) {
this.props.userLoading(false);
this.props.authUser(true);
this.props.storeUser(user.providerData);
this.props.history.push('/dasboard');
} else {
this.props.userLoading(false);
this.props.authUser(false);
this.props.history.push('/');
}
});
}
componentWillUnMount(){
this.unSubscribe();
}
render(){
const { authenticated, loading } = this.state;
return(
<div>
<Header />
<Router>
<div>
<Route path='/' exact component={Home}/>
<Route path='/dashboard' component={Dashboard} />
</div>
</Router>
<Footer />
</div>
)
}
}
const mapStateToProps = state => {
const user = state.getIn(['user', 'data'], List());
const authenticate = state.getIn(['user', 'auth'], false);
const userLoading = state.getIn(['user', 'loading'], false);
return {
user,
authenticate,
userLoading
};
}
const actionsToProps = {
storeUser: actions.storeUser,
authUser: actions.authUser,
userLoading: actions.userLoading
}
export default withRouter(connect(mapStateToProps, actionsToProps)(App))
auth.js:
import firebase from './firebase';
export const auth = firebase.auth();
export const logout = firebase.auth().signOut();
export const githubProvider = firebase.firebase_.auth.GithubAuthProvider();
export const twitterProvider = new firebase.firebase_.auth.TwitterAuthProvider();
export const facebookProvider = new firebase.firebase_.auth.FacebookAuthProvider();
I am using firebase version 5.8.3.
I have tried with many possibilities like using async await, setTimeout but none of them are working.
You seem to be signing out in auth.js:
firebase.auth().signOut();
Firebase queues the sign out operation and runs it when the Auth instance is ready.

Integrating Dispatch Actions in Container Component Pattern

So I'm completely confused on how to integrate the Container and Component Pattern. I've been reviewing examples all morning and nothing seems to be clicking. How I have been worked with React previously on my first project was fetch the data within my view components and then pass that data down as props using the #connect which works, but in an "automagically" way to me at this time.
import React;
...
import {action} from 'path/to/action.js';
#connect((store) => {return{ key: store.property}});
export class Component{
componentWillMount(){
this.props.dispatch(action());
}
}
As I'm working more with React I want to learn the more "correct" way of building out with Redux and understand on a deeper level what is happening.
What I have setup is
index.jsx (This renders all of my HOCs)
|
App.jsx (Container)
|
Auth.jsx (Component)
|
Layout.jsx (Component) - Contains app content
--or--
AuthError.jsx (Component) - 401 unauthenticated error page
Authentication is handled through an outside resource so this app will not control anything with Logging in or out. There will be no log in/out states simply receiving an object from an API that identifies the User Role & Authenticated Boolean.
What I would like to happen is when the App loads, it will fetch data from a mock API, JSON Server. From there it will render the Auth component. The Auth component will take in props from App.jsx and either render the Layout.jsx or AuthError.jsx.
Where I'm running into issues is how this should be integrated. I'm going to omit lines of code I don't think absolutely pertain to the question.
store.js
import { applyMiddleware, combineReducers, createStore } from 'redux';
import thunk from 'redux-thunk';
import { createLogger } from 'redux-logger';
import promise from 'redux-promise-middleware';
import { composeWithDevTools } from 'redux-devtools-extension';
import reducer from './reducers';
const middleware = applyMiddleware(promise(), thunk, createLogger());
export default createStore(reducer, composeWithDevTools(middleware));
index.jsx
import React from 'react';
import store from './store.js';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './containers/App.jsx';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
App.jsx
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { authenticateUser } from '../actions/authActions.js';
import Auth from '../components/Auth.jsx';
class App extends Component {
constructor(props) {
super(props);
this.state = {
authenticated: false // this needs to be set
};
}
componentWillMount() {
console.log('APP PROPS', this.props);
// this.props.actions.authenticateUser();
authenticateUser(); // this runs but doesn't run the dispatch function
// What I think needs to happen here Dispatch an Action and then setState referring back to how I would previous build with React Redux.
}
render() {
return (
<Auth app_name={ApplicationName} authenticated={this.state.authenticated} {...this.props} />
);
}
}
const mapStateToProps = state => {
console.log('redux store auth state', state);
return {
auth: state.auth
};
};
const mapDispatchToProps = dispatch => {
return { actions: bindActionCreators(authenticateUser, dispatch) };
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
Auth.jsx
import React from 'react';
import { Route } from 'react-router-dom';
import AuthError from './AuthError.jsx';
import Layout from './Layout.jsx';
export default function Auth(props) {
console.log('AUTH PROPS', props);
const renderLayout = () => {
if (props.authenticated == true) {
return <Layout app_name={props.app_name} />;
} else {
return <AuthError />;
}
};
return <Route path="/" render={renderLayout} />;
}
authReducer.js
export default function reducer(
state = {
authenticated: null
},
action
) {
switch (action.type) {
case 'AUTH_SUCCESSFUL': {
return {
...state,
authenticated: action.payload.authenticated
};
break;
}
case 'AUTH_REJECTED': {
return {
...state,
authenticated: false
};
}
}
return state;
}
authActions.js
import axios from 'axios';
export function authenticateUser() {
console.log('authenticate user action has been called');
return function(dispatch) {
// nothing runs within this block so it's leading me to believe nothing is being `dispatch`ed
console.log('dispatch', dispatch);
axios
.get('localhost:3004/auth')
.then(response => {
dispatch({ type: 'AUTH_SUCCESSFUL', payload: response.data });
console.log('response', response);
})
.catch(err => {
dispatch({ type: 'AUTH_REJECTED', payload: err });
console.log('error', err);
});
};
}
Right now inside of App.jsx I can console the state of the authReducer and I can call authenticateUser() in my actions. But when I call authenticateUser() the return dispatch function doesn't run. Should I be dispatching the auth action in App.jsx? Or should I be dispatching the auth in Auth.jsx as a prop to then have App.jsx fetch the data? Just a bit lost on breaking this apart and what piece should be doing what work.
I'll do a brief explanation about it to help you to understand those patterns and don't get in confusion anymore (I hope).
So, let's forget reducers for a moment to focus on container, action creator and component pattern.
Component
A lot of people implement components by wrong way when using it with redux application.
A better component approach for redux is, implement it with stateless pattern (see Functional Components). Let's see in practice:
// components/Subscribe.js
import React from 'react'
import PropTypes from 'prop-types'
const Subscribe = ({text, confirmSubscription}) =>
<div>
<p>{text}</p>
<button onClick={confirmSubscription}>Confirm</button>
</div>
Subscribe.propTypes = {
subtitle: PropTypes.string.isRequired
}
Subscribe.defaultProps = {
subtitle: ''
}
export default Subtitle
This allows you to optimize component footprint because they have less features than stateful components (or class components), so you will win some performance and keep focused on component objective.
Container
In other hand, Container is a kind of component with some logical implementation. Container is a pattern created to bind React and Redux, because both should't interact directly. This means, a Container render the component, handle some component events (for example, form onSubmit) and feed components with application state. So, the Container is the best place to interact with Redux. (react-redux)[https://github.com/reactjs/react-redux] and Redux make this task a bit easier. So a simple Container to feed and capture interactions on Subscribe component could be like this:
// containers/SubscribeContainer.js
import React from 'react'
import PropTypes from 'prop-types'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { confirmSubscription } from 'actions/subscription'
import Subscribe from 'components/Subscribe'
const mapStateToProps = state => ({
text: state.subscription.text
})
const mapDispatchToProps = dispatch =>
bindActionCreators({
confirmSubscription
}, dispatch)
const Container = connect(mapStateToProps, mapDispatchToProps)
export default Container(Subscribe)
Action Creator
An action creator (or action creators), is just a collection of or a function where return an action. Simple like that:
// actions/subscription
export const CONFIRM_SUBSCRIPTION = 'actions.confirmSubscription'
export function confirmSubscription() {
return {
type: CONFIRM_SUBSCRIPTION
}
}
For now, we have the triad pattern, Component, Container and Action Creator implemented, from here, you just need two more things to make this working with Redux.
Create a subscription store.
Handle CONFIRM_SUBSCRIPTION (in case to update app's state)
Return a new state
The magic will happen when you return a new state from any reducer, the mapStateToProps will be called and you will receive the new state as argument and from there, React will update your components when necessary, in case of those components are stateless, PureComponent (works only with single level states and props) or custom shouldComponentUpdate.
Another thing to keep on mind is to not do fetch or async execution inside Components, Containers and Action Creators, instead, you can use middleware like redux-thunk to compose a custom middeware to capture actions and handle that before be sent to reducers.
your authenticateUser returns a function, you need to literally run the function. The right way to do that is to add a property in your mapDispatchToProps
const mapDispatchToProps = dispatch => {
return { authenticateUser: () => dispatch(authenticateUser()) };
};
Then, in your componentWillMount function, call
this.props.authenticateUer()
Check this

Getting a value from a particular state before component renders using react/redux?

I am using react/redux with a nodejs(express)/mongodb back-end incase that helps.
What I want to happen here is that if a user tries to go to edit a post that does not belong to them I want them to be re routed immediately and never see that page.
For example. User "A" goes to route localhost:8080/posts/post_id/edit, but that post belongs to user "B". I want User A to immediately get re routed back to that post or localhost:8080/posts/post_id.
In my code I can get the user through a action called getUser() which sends an axios.get request to the back-end to get the current user who is logged in. I am using JWT token. Not sure if this is information needed or not.
Here is the code to show you what I am trying to do.
import React , { Component } from 'react';
import { bindActionCreators } from 'redux';
import * as actions from '../../actions/posts_actions';
import * as actionsIndex from '../../actions/index';
import { reduxForm, Field } from 'redux-form';
import {connect} from 'react-redux';
import {Link} from 'react-router-dom';
class EditPost extends Component {
componentWillMount() {
if(this.props.auth) {
console.log(this.props.auth); // -> returns true
this.props.getUser(); // -> this fires off
}
}
componentDidMount() {
const {id} = this.props.match.params;
this.props.getOnePost(id);
if(this.props.auth){
if(this.props.user._id !== this.props.post.author.id){
this.props.history.push(`/posts/${id}`);
}
}
}
renderField(field) {
const { meta: {touched, error} } = field;
const className = `form-group ${touched && error ? 'has-danger' : ''}`;
return (
<div className={className}>
<label><strong>{field.label}:</strong></label>
<input
className="form-control"
type={field.type}
{...field.input}
/>
<div className="text-help">
{ touched ? error : ''}
</div>
</div>
)
}
onSubmit(values) {
const {id} = this.props.match.params;
this.props.updatePost(values, id, () => {
this.props.history.push(`/posts/${id}`);
});
}
render() {
const {handleSubmit} = this.props;
const {id} = this.props.match.params;
console.log(this.props.user); // -> shows me the user after three nulls
return (
<form onSubmit={handleSubmit(this.onSubmit.bind(this))}>
<Field
label="Title"
name="title"
type="text"
component={this.renderField}
/>
<Field
label="Content"
name="content"
type="text"
component={this.renderField}
/>
<button type="submit" className="btn btn-success">Submit</button>
<Link to={`/posts/${id}`} className="btn btn-danger">Cancel</Link>
</form>
);
}
}
function validate(values) {
const errors = {};
if(!values.title) {
errors.title = "Enter a title!";
}
if(!values.content) {
errors.content = "Enter some content please!";
}
return errors;
}
function mapStateToProps({ posts, auth, user }, ownProps) {
return {
initialValues: posts[ownProps.match.params.id],
post: posts[ownProps.match.params.id],
auth: auth.authenticated,
user: user
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({...actions, ...actionsIndex}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(reduxForm({
validate,
form: 'editform'
})(EditPost));
Here are the console.log statements:
Here is an edit of the index.js page , is there some way I could update the user state here?:
"use strict"
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import {BrowserRouter, Route, Switch} from 'react-router-dom';
import { applyMiddleware, createStore } from 'redux';
import reduxThunk from 'redux-thunk';
import reducers from './reducers/index';
import App from './components/app';
import '../style/style.css';
import Home from './components/pages/home';
import Header from './components/header';
import Footer from './components/footer';
import RequireAuth from './components/auth/require_auth';
import RequireUnAuth from './components/auth/require_unauth';
import Signin from './components/auth/signin';
import Signup from './components/auth/signup';
import Signout from './components/auth/signout';
import Posts from './components/pages/posts';
import {AUTH_USER} from './actions/types';
const createStoreWithMiddleware = applyMiddleware(reduxThunk)(createStore);
const store = createStoreWithMiddleware(reducers);
const token = localStorage.getItem('token');
if(token) {
store.dispatch({ type: AUTH_USER });
}
const Routes = (
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
)
ReactDOM.render(Routes, document.querySelector('.container'));
If this.props.getUser() is an async (thunk-ed) action creator, you're not guaranteed to have the result of that available by the time you reach componentDidMount
Add a null check for this.props.user in componentDidMount before you attempt to access fields on it.
Something you could do is move
if(this.props.user._id !== this.props.post.author.id){
this.props.history.push(`/posts/${id}`);
}
into componentWillReceiveProps
componentWillReceiveProps ({ user: nextUser }) {
const { history, match, user: currentUser, post } = this.props
const { id } = match.params
/* if you didn't previously have currentUser (i.e. after first load) - !currentUser
* or the user has changed - nextUser !== currentUser*/
if (!currentUser || nextUser !== currentUser) { // probably shouldn't use === here
/* the component might update before `this.props.getUser()` "returns", so make sure this is the
* update we are looking for by ensuring that we have nextUser - nextUser &&
* then get the id (nextUser._id), and run the check (!== post.author.id)*/
if (nextUser && (nextUser._id !== post.author.id)) {
history.push(`/posts/${id}`)
}
}
}
Here's a little light Friday reading on componentWillReceiveProps - might clear some things up.
A couple things regarding your edit:
An HoC might be a good idea if you have a lot of components that rely on user
AuthComponent HoC concept
class AuthComponent extends React.Component {
componentDidMount() {
if (!this.props.user) {
this.props.getUser()
}
}
render() {
// you could inject `user` into children here, but I would suggest that you
// instead store the result of `getUser` into a store like `redux` or `flux` and fetch from that in child components
return this.props.children
}
}
Obviously you'll need to use connect and grab user out of state for this component. You would use it like
class SomeComponent extends React.Component {
render () {
return (
<AuthComponent>
<WrappedComponent/> // <- this is what's rendered
</AuthComponent>
)
}
}
you're using react-router, though, so a better solution would be to incorporate it into your routing
<Route path="" component={AuthComponent}>
<Route path="some-path" component={WrappedComponent}/>
</Route>
like I said in the comment, cache and return user from your store... although it looks like you're already doing this
don't assume that you'll always have access to user - make sure you're null checking it
The code that you are using in your componentWillMount does not belong here.
this.props.getUser();
You better create an action redux-thunk creator which deals with promise + async actions and only then return the result through the state/dispatch mechanism to be later used in componentDidMount lifecycle hook.
Redux thunk example to call API, please take a look at redux-thunk docs:
const store = createStore(
reducer,
applyMiddleware(thunk.withExtraArgument(api))
)
// later
function fetchUser(id) {
return (dispatch, getState, api) => {
// you can use api here
}
}
To pass multiple things, just wrap them in a single object and use destructuring:
const store = createStore(
reducer,
applyMiddleware(thunk.withExtraArgument({ api, whatever }))
)
// later
function fetchUser(id) {
return (dispatch, getState, { api, whatever }) => {
// you can use api and something else here here
}
}

Resources