How to properly ask for props on component - reactjs

I have a component SinglePost that is called when another component Post is clicked in the main page, and also through path /post/:id. The thing is, in SinglePost, I call an API through Redux on componentDidMount, and then ask for it on componentWillReceiveProps. And when you access the component from the URL /post/:id, the props throws undefined two times after it loads.
I was following a React/Redux tutorial, to make a CRUD web app with API calls. And, in that project, the props loads fine in both cases (through main page navigation, and from URL). I did exactly the same thing and it doesn't work. Is this a problem, or does it work this way?
This is the SinglePost component:
import { connect } from 'react-redux';
import { mostrarPublicacion } from '../../actions/publicacionesActions';
state = {
publicacion: {},
}
componentDidMount() {
const {id} = this.props.match.params;
//this is the action defined on PublicacionesActions
this.props.mostrarPublicacion(id);
}
componentWillReceiveProps(nextProps, nextState) {
const {publicacion} = nextProps;
this.setState({
publicacion
})
}
const mapStateToProps = state => ({
publicacion: state.publicaciones.publicacion
})
export default connect(mapStateToProps, {mostrarPublicacion}) (SinglePost);
PublicacionesActions:
export const mostrarPublicacion = (id) => async dispatch => {
const respuesta = await axios.get(`http://www.someapi.com:8000/api/publicacion/${id}`);
dispatch({
type: MOSTRAR_PUBLICACION,
payload: respuesta.data
})
}
I debugged this and it actually returns the publication. In fact, it renders properly in the SinglePost component. But at first it loads undefined when accessing the component through the url.
RootReducer:
import { combineReducers } from 'redux';
import publicacionesReducer from './publicacionesReducer';
import seccionesReducer from './seccionesReducer';
export default combineReducers({
publicaciones: publicacionesReducer,
secciones: seccionesReducer
});
PublicacionesReducer:
export default function (state = initialState, action) {
switch(action.type) {
case MOSTRAR_PUBLICACION:
return {
...state,
publicacion: action.payload
}
default:
return state
}
}
This is the Router component (wrapped into <Provider store={store}>):
<Switch>
<Route exact path='/post/:id' component={SinglePost} />
</Switch>
I actually ask for (!this.state.publication) and return null, to render the component. It is working, but it is necesary?

Related

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 :)

Testing Redux Enzyme: Component doesn't re-render after state has changed

I have a connected component (to Redux store) called DrawerAvatar, that I export for testing purpose (Enzyme + Jest) both the connected and non connected version.
Basically, I want to test that my DrawerAvatar render the user avatar when my Redux state isAuthenticated is true, and it renders a logo picture when isAuthenticated is false.
DrawerAvatar.js
export class DrawerAvatar extends React.Component {
render () {
const avatarSrc = this.props.isAuthenticated ?
'http://user-avatar.png'
) : (
'http://logo.png'
);
return (
<StyledAvatarContainer>
<StyledAvatar src={avatarSrc} />
</StyledAvatarContainer>
);
}
}
const mapStateToProps = state => ({
isAuthenticated: state.authReducer.isAuthenticated
});
export default compose(
connect(mapStateToProps, null)
)(DrawerAvatar);
And in my test, I'm using the non-connected DrawerAvatar, and connect it to my real Redux store via the Provider like this: (initiale state: isAuthenticated: false)
DrawerAvatar.test.js:
import React from 'react';
import { shallow } from 'enzyme';
import { Provider } from 'react-redux';
import store from '../../store';
import connectedDrawerAvatar, { DrawerAvatar } from './DrawerAvatar';
describe('Header > DrawerAvatar: component', () => {
it('should render logo for the DrawerAvatar if not authenticated, and the user avatar if authenticated', () => {
const wrapper = shallow(<Provider store={store}><DrawerAvatar /></Provider>);
console.log(wrapper.dive().debug());
// Output:
// <StyledAvatarContainer>
// <StyledAvatar src="https://logo.png" />
// </StyledAvatarContainer>
const StyledAvatarSrc = wrapper.dive().find('StyledAvatar').prop('src');
expect(StyledAvatarSrc).toBe('https://logo.png'); // assertion passed
store.dispatch({ type: 'LOGIN_WITH_EMAIL_REQUESTED_TEST' });
// the state has been correctly updated, now isAuthenticated: true
console.log(wrapper.dive().debug());
// Output: same as above, whereas it should be:
// <StyledAvatarContainer>
// <StyledAvatar src="https://user-avatar.png" />
// </StyledAvatarContainer>
expect(StyledAvatarSrc).toBe('https://user-avatar.png'); // assertion failed
});
});
And here is my authReducer:
authReducer.js
const initialState = {
isAuthenticated: false
};
export default function authReducer (state = initialState, action) {
switch (action.type) {
case 'LOGIN_WITH_EMAIL_REQUESTED_TEST':
return {
...state,
isAuthenticated: true,
};
default:
return state;
}
}
So basically, I have a real action with type of LOGIN_WITH_EMAIL_REQUESTED that will call a bunch of Redux-saga with Axios, but for testing purpose, I just added to my real authReducer a LOGIN_WITH_EMAIL_REQUESTED_TEST case that will set the state isAuthenticated to true to avoid Axios calls etc...Not sure if it's a good way to do things though..lol
I've tried in vain to force the component to update with wrapper.update()...
I've also looked at redux-mock-store, but it seems like you cannot modify the state and only deals with actions and not the states.
I just start writing my first React test so...thank you !
Basically, I want to test that my DrawerAvatar render the user avatar when my Redux state isAuthenticated is true, and it renders a logo picture when isAuthenticated is false.
I would recommend not connecting the entire connected component and trying to bother with Redux at all. You can achieve your expected result with the following:
describe('Header > DrawerAvatar: component', () => {
it('should render logo for the DrawerAvatar if not authenticated, and the user avatar if authenticated', () => {
let wrapper = shallow(<DrawerAvatar isAuthenticated={false} />);
let StyledAvatarSrc = wrapper.find('StyledAvatar').prop('src');
expect(StyledAvatarSrc).toBe('https://logo.png');
wrapper = shallow(<DrawerAvatar isAuthenticated={true} />);
StyledAvatarSrc = wrapper.find('StyledAvatar').prop('src');
expect(StyledAvatarSrc).toBe('https://user-avatar.png');
});
});
Then, you can write separate tests for each of the pieces involved, for example your mapStateToProps function, which is really just a simple function that returns an object based on its input. And another simple test of your authReducer, etc

mapStateToProps passing props as empty object

I'm trying to add Redux to my React Native application. There is a Decider app that should control whether or not the <Intro /> is shown or <Content />.
I've never had any issues with linking redux before and have tried different variations so I must be doing something wrong here. FYI The only reason I have a <Decider /> component is because <App /> is the root component and following my understanding you can't have a Provider and connect() on the same component.
Props is coming through as an empty object. What am I doing wrong?
App.js
import Intro from './pages/Intro';
import Content from './pages/Content';
const store = createStore(rootReducer)
export default class App extends Component {
render() {
return (
<Provider store={store}>
<Decider />
</Provider>
)
}
}
class Decider extends Component {
render() {
return this.props.showIntro ? <Intro /> : <Content />
}
}
const mapStateToProps = state => {
return {
showIntro: state.showIntro
}
}
connect(mapStateToProps)(Decider)
./reducers/index.js
[...auth reducer...]
const viewControl = (state = true, action) => {
switch (action.type) {
case ON_INTRO_COMPLETION:
return action.payload
default:
return state;
}
};
export default rootReducer = combineReducers({ auth, viewControl });
./actions/index.js
export const onIntroCompletion = bool => {
return {
type: 'ON_INTRO_COMPLETION',
payload: false
}
}
From redux docs
combineReducers takes an object full of slice reducer functions, and
creates a function that outputs a corresponding state object with the
same keys.
As state is immutable the new state is constructed with the keys you specify in combineReducers, so if
your reducer doesn't fulfill the schema of your state you will lose properties.
You might want to try something like this:
export default rootReducer = combineReducers({
auth,
viewControl,
showIntro: showIntro => showIntro
});
Your action returns: payload: true, and reducer supplies this as the entire state object (true or false).
But, when you connect() your component, you assign try to pull from state.showIntro.
In your reducer, try this:
case ON_INTRO_COMPLETION:
return {showIntro: action.payload};

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

React/Redux - dispatch action on app load/init

I have token authentication from a server, so when my Redux app is loaded initially I need make a request to this server to check whether user is authenticated or not, and if yes I should get token.
I have found that using Redux core INIT actions is not recommended, so how can I dispatch an action, before app is rendered?
You can dispatch an action in Root componentDidMount method and in render method you can verify auth status.
Something like this:
class App extends Component {
componentDidMount() {
this.props.getAuth()
}
render() {
return this.props.isReady
? <div> ready </div>
: <div>not ready</div>
}
}
const mapStateToProps = (state) => ({
isReady: state.isReady,
})
const mapDispatchToProps = {
getAuth,
}
export default connect(mapStateToProps, mapDispatchToProps)(App)
All of the answers here seem to be variations on creating a root component and firing it in the componentDidMount. One of the things I enjoy most about redux is that it decouples data fetching from component lifecycles. I see no reason why it should be any different in this case.
If you are importing your store into the root index.js file, you can just dispatch your action creator(let's call it initScript()) in that file and it will fire before anything gets loaded.
For example:
//index.js
store.dispatch(initScript());
ReactDOM.render(
<Provider store={store}>
<Routes />
</Provider>,
document.getElementById('root')
);
I've not been happy with any solutions that have been put forward for this, and then it occurred to me that I was thinking about classes needing to be rendered. What about if I just created a class for startup and then push things into the componentDidMount method and just have the render display a loading screen?
<Provider store={store}>
<Startup>
<Router>
<Switch>
<Route exact path='/' component={Homepage} />
</Switch>
</Router>
</Startup>
</Provider>
And then have something like this:
class Startup extends Component {
static propTypes = {
connection: PropTypes.object
}
componentDidMount() {
this.props.actions.initialiseConnection();
}
render() {
return this.props.connection
? this.props.children
: (<p>Loading...</p>);
}
}
function mapStateToProps(state) {
return {
connection: state.connection
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(Actions, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Startup);
Then write some redux actions to async initialise your app. Works a treat.
If you are using React Hooks, one single-line solution is
useEffect(() => store.dispatch(handleAppInit()), []);
The empty array ensures it is called only once, on the first render.
Full example:
import React, { useEffect } from 'react';
import { Provider } from 'react-redux';
import AppInitActions from './store/actions/appInit';
import store from './store';
export default function App() {
useEffect(() => store.dispatch(AppInitActions.handleAppInit()), []);
return (
<Provider store={store}>
<div>
Hello World
</div>
</Provider>
);
}
Update 2020:
Alongside with other solutions, I am using Redux middleware to check each request for failed login attempts:
export default () => next => action => {
const result = next(action);
const { type, payload } = result;
if (type.endsWith('Failure')) {
if (payload.status === 401) {
removeToken();
window.location.replace('/login');
}
}
return result;
};
Update 2018: This answer is for React Router 3
I solved this problem using react-router onEnter props. This is how code looks like:
// this function is called only once, before application initially starts to render react-route and any of its related DOM elements
// it can be used to add init config settings to the application
function onAppInit(dispatch) {
return (nextState, replace, callback) => {
dispatch(performTokenRequest())
.then(() => {
// callback is like a "next" function, app initialization is stopped until it is called.
callback();
});
};
}
const App = () => (
<Provider store={store}>
<IntlProvider locale={language} messages={messages}>
<div>
<Router history={history}>
<Route path="/" component={MainLayout} onEnter={onAppInit(store.dispatch)}>
<IndexRoute component={HomePage} />
<Route path="about" component={AboutPage} />
</Route>
</Router>
</div>
</IntlProvider>
</Provider>
);
With the redux-saga middleware you can do it nicely.
Just define a saga which is not watching for dispatched action (e.g. with take or takeLatest) before being triggered. When forked from the root saga like that it will run exactly once at startup of the app.
The following is an incomplete example which requires a bit of knowledge about the redux-saga package but illustrates the point:
sagas/launchSaga.js
import { call, put } from 'redux-saga/effects';
import { launchStart, launchComplete } from '../actions/launch';
import { authenticationSuccess } from '../actions/authentication';
import { getAuthData } from '../utils/authentication';
// ... imports of other actions/functions etc..
/**
* Place for initial configurations to run once when the app starts.
*/
const launchSaga = function* launchSaga() {
yield put(launchStart());
// Your authentication handling can go here.
const authData = yield call(getAuthData, { params: ... });
// ... some more authentication logic
yield put(authenticationSuccess(authData)); // dispatch an action to notify the redux store of your authentication result
yield put(launchComplete());
};
export default [launchSaga];
The code above dispatches a launchStart and launchComplete redux action which you should create. It is a good practice to create such actions as they come in handy to notify the state to do other stuff whenever the launch started or completed.
Your root saga should then fork this launchSaga saga:
sagas/index.js
import { fork, all } from 'redux-saga/effects';
import launchSaga from './launchSaga';
// ... other saga imports
// Single entry point to start all sagas at once
const root = function* rootSaga() {
yield all([
fork( ... )
// ... other sagas
fork(launchSaga)
]);
};
export default root;
Please read the really good documentation of redux-saga for more information about it.
Here's an answer using the latest in React (16.8), Hooks:
import { appPreInit } from '../store/actions';
// app preInit is an action: const appPreInit = () => ({ type: APP_PRE_INIT })
import { useDispatch } from 'react-redux';
export default App() {
const dispatch = useDispatch();
// only change the dispatch effect when dispatch has changed, which should be never
useEffect(() => dispatch(appPreInit()), [ dispatch ]);
return (<div>---your app here---</div>);
}
I was using redux-thunk to fetch Accounts under a user from an API end-point on app init, and it was async so data was coming in after my app rendered and most of the solutions above did not do wonders for me and some are depreciated. So I looked to componentDidUpdate(). So basically on APP init I had to have accounts lists from API, and my redux store accounts would be null or []. Resorted to this after.
class SwitchAccount extends Component {
constructor(props) {
super(props);
this.Format_Account_List = this.Format_Account_List.bind(this); //function to format list for html form drop down
//Local state
this.state = {
formattedUserAccounts : [], //Accounts list with html formatting for drop down
selectedUserAccount: [] //selected account by user
}
}
//Check if accounts has been updated by redux thunk and update state
componentDidUpdate(prevProps) {
if (prevProps.accounts !== this.props.accounts) {
this.Format_Account_List(this.props.accounts);
}
}
//take the JSON data and work with it :-)
Format_Account_List(json_data){
let a_users_list = []; //create user array
for(let i = 0; i < json_data.length; i++) {
let data = JSON.parse(json_data[i]);
let s_username = <option key={i} value={data.s_username}>{data.s_username}</option>;
a_users_list.push(s_username); //object
}
this.setState({formattedUserAccounts: a_users_list}); //state for drop down list (html formatted)
}
changeAccount() {
//do some account change checks here
}
render() {
return (
<Form >
<Form.Group >
<Form.Control onChange={e => this.setState( {selectedUserAccount : e.target.value})} as="select">
{this.state.formattedUserAccounts}
</Form.Control>
</Form.Group>
<Button variant="info" size="lg" onClick={this.changeAccount} block>Select</Button>
</Form>
);
}
}
const mapStateToProps = state => ({
accounts: state.accountSelection.accounts, //accounts from redux store
});
export default connect(mapStateToProps)(SwitchAccount);
If you're using React Hooks, you can simply dispatch an action by using React.useEffect
React.useEffect(props.dispatchOnAuthListener, []);
I use this pattern for register onAuthStateChanged listener
function App(props) {
const [user, setUser] = React.useState(props.authUser);
React.useEffect(() => setUser(props.authUser), [props.authUser]);
React.useEffect(props.dispatchOnAuthListener, []);
return <>{user.loading ? "Loading.." :"Hello! User"}<>;
}
const mapStateToProps = (state) => {
return {
authUser: state.authentication,
};
};
const mapDispatchToProps = (dispatch) => {
return {
dispatchOnAuthListener: () => dispatch(registerOnAuthListener()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
Same solution as Chris Kemp mentions above. Could be even more generic, just a canLift func not tied to redux?
interface Props {
selector: (state: RootState) => boolean;
loader?: JSX.Element;
}
const ReduxGate: React.FC<Props> = (props) => {
const canLiftGate = useAppSelector(props.selector);
return canLiftGate ? <>{props.children}</> : props.loader || <Loading />;
};
export default ReduxGate;
Using: Apollo Client 2.0, React-Router v4, React 16 (Fiber)
The answer selected use old React Router v3. I needed to do 'dispatch' to load global settings for the app. The trick is using componentWillUpdate, although the example is using apollo client, and not fetch the solutions is equivalent.
You don't need boucle of
SettingsLoad.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import {bindActionCreators} from "redux";
import {
graphql,
compose,
} from 'react-apollo';
import {appSettingsLoad} from './actions/appActions';
import defQls from './defQls';
import {resolvePathObj} from "./utils/helper";
class SettingsLoad extends Component {
constructor(props) {
super(props);
}
componentWillMount() { // this give infinite loop or no sense if componente will mount or not, because render is called a lot of times
}
//componentWillReceiveProps(newProps) { // this give infinite loop
componentWillUpdate(newProps) {
const newrecord = resolvePathObj(newProps, 'getOrgSettings.getOrgSettings.record');
const oldrecord = resolvePathObj(this.props, 'getOrgSettings.getOrgSettings.record');
if (newrecord === oldrecord) {
// when oldrecord (undefined) !== newrecord (string), means ql is loaded, and this will happens
// one time, rest of time:
// oldrecord (undefined) == newrecord (undefined) // nothing loaded
// oldrecord (string) == newrecord (string) // ql loaded and present in props
return false;
}
if (typeof newrecord ==='undefined') {
return false;
}
// here will executed one time
setTimeout(() => {
this.props.appSettingsLoad( JSON.parse(this.props.getOrgSettings.getOrgSettings.record));
}, 1000);
}
componentDidMount() {
//console.log('did mount this props', this.props);
}
render() {
const record = resolvePathObj(this.props, 'getOrgSettings.getOrgSettings.record');
return record
? this.props.children
: (<p>...</p>);
}
}
const withGraphql = compose(
graphql(defQls.loadTable, {
name: 'loadTable',
options: props => {
const optionsValues = { };
optionsValues.fetchPolicy = 'network-only';
return optionsValues ;
},
}),
)(SettingsLoad);
const mapStateToProps = (state, ownProps) => {
return {
myState: state,
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators ({appSettingsLoad, dispatch }, dispatch ); // to set this.props.dispatch
};
const ComponentFull = connect(
mapStateToProps ,
mapDispatchToProps,
)(withGraphql);
export default ComponentFull;
App.js
class App extends Component<Props> {
render() {
return (
<ApolloProvider client={client}>
<Provider store={store} >
<SettingsLoad>
<BrowserRouter>
<Switch>
<LayoutContainer
t={t}
i18n={i18n}
path="/myaccount"
component={MyAccount}
title="form.myAccount"
/>
<LayoutContainer
t={t}
i18n={i18n}
path="/dashboard"
component={Dashboard}
title="menu.dashboard"
/>

Resources