React/Redux connect does not fire mapStateToProps on store change - javascript

I am experiencing an issue with Redux (used along with React). Here is my code:
/// <reference path="../../typings/index.d.ts"/>
import "./polyfill.ts";
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { createStore, combineReducers } from 'redux'
import { Provider, dispatch } from 'react-redux'
import { Router, Route, browserHistory, IndexRedirect } from 'react-router'
import { syncHistoryWithStore, routerReducer } from 'react-router-redux'
import { Form, Input } from './components/form.tsx'
import app from './reducers.ts'
const store = createStore(
combineReducers({
app,
routing: routerReducer
}),
{},
window.devToolsExtension && window.devToolsExtension()
);
import App from './views/app.tsx';
import Reminders from './views/reminders.tsx';
import Calendar from './views/calendar.tsx';
import Settings from './views/settings.tsx';
import Groups from './views/groups.tsx';
import Courses from './views/courses.tsx';
import Homework from './views/homework.tsx';
// Create an enhanced history that syncs navigation events with the store
const history = syncHistoryWithStore(browserHistory, store);
class Application extends React.Component<any,any> {
constructor(props){
super(props);
}
render() {
var socket = io();
socket.on('connect', function () {
setTimeout(function(){
store.dispatch({
type: "SOCKET_ESTABLISHED",
socket: socket
});
}, 1000);
});
socket.on('DATA', function(data){
console.log("DATA");
console.log(data);
})
console.log(Groups);
return <Provider store={store}>
<Router history={history}>
<Route path="/" component={App}>
<IndexRedirect to="/reminders" />
<Route path="reminders" component={Reminders}/>
<Route path="calendar" component={Calendar}/>
<Route path="settings" component={Settings}>
<Route path="groups" component={Groups}/>
</Route>
<Route path="homework" component={Homework} />
</Route>
</Router>
</Provider>
}
}
ReactDOM.render(
<Application/>,
document.getElementById('mount')
);
My reducer file looks like the following:
export default function app(state={
connection: {
socket: null,
online: false
}
}, action: any) {
console.log(action);
switch(action.type){
case "SOCKET_ESTABLISHED":
console.log(state.connection);
return Object.assign(state, {
connection: {
socket: action.socket,
online: true
}
});
default:
return state;
}
};
And my App component is the following:
import * as React from 'react';
import { connect } from 'react-redux';
import Top from '../components/top.tsx';
import Nav from '../components/nav.tsx';
class AppClass extends React.Component<any,any> {
constructor(props){
super(props);
}
render() : JSX.Element {
return <div>
<Top/>
<Nav/>
{this.props.connection.online ? this.props.children : "Offline." }
</div>
}
}
var App = connect(function(state){
return {
connection: state.app.connection
};
})(AppClass);
export default App;
So let me be clear, I am using the connect function to sync store and App props. Upon page load, a socket connection is created and the action SOCKET_ESTABLISHED which changes store.connection.online to true. Everything works fine, the Redux dev tools show that the store is correctly updated and online is now true. If I look with the React dev tools the Connect(AppClass) component, its storeState is up to date, but its child component, which is AppClass, does not have the right props, this.props.app.connection.online is false where it should be true. I have seen that it could come from a state mutation, but using Object.assign a new store is returned (The function is the MDN Polyfill).
Hope I was clear and thank you !
(Note that I am using Typescript for compiling although I might have been neglecting some interface definitions)

You're modifying the state object inside of the reducer. You need to copy state to a new object, and then modify the new one.
Update this:
return Object.assign(state, {
connection: {
socket: action.socket,
online: true
}
});
To this:
return Object.assign({}, state, {
connection: {
socket: action.socket,
online: true
}
});
// or this
return {
...state,
connection: {
socket: action.socket,
online: true
}
}
Redux receives the new state from the reducer and compares it to the previous state. If you modify the state directly, the new state and previous state are the same object. It's similar to doing this.
var state = { foo: 'bar' };
state.test = 'test';
var newState = state;
// state === newState # true
Instead, you want to copy the object before modifying
var state = { foo: 'bar' };
var newState = { ...state, test: 'test' };
// state === newState # false

mapStateToProps function must return new state object to triger rerender of the component.
Try changing
return state
to
return {...state}

Related

React Redux this.props always return undefined

I'm now at React and I'm doing some apps to study, learn more about. Aand right now I'm trying to add the logged user info to redux state, but when I try to check the value of this.props.user my app always returns undefined.
My reducer.js
import { LOG_USER } from '../actions/actions';
let initialState = {
user: {
userName: '',
imageUrl: ''
}
}
const userInfo = (state = initialState, action) => {
switch (action.type) {
case LOG_USER:
return {
...state,
user: action.user
};
default:
return state;
}
}
const reducers = userInfo;
export default reducers;
My actions.js
export const LOG_USER = 'LOG_USER';
My SignupGoogle.js component:
import React, { Component } from 'react';
import Button from '#material-ui/core/Button';
import firebase from '../../config/firebase';
import { connect } from 'react-redux'
import { LOG_USER } from '../../actions/actions';
import './SignupGoogle.css'
class SignupGoogle extends Component {
constructor(props) {
super(props);
}
signup() {
let provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider).then(function(result) {
console.log('---------------------- USER before login')
console.log(this.props.user)
let user = {
userName: result.user.providerData[0].displayName,
imageUrl: result.user.providerData[0].photoURL
}
console.log(user)
this.props.logUser(user)
console.log('---------------------- USER after login')
console.log(this.props.user)
}).catch((error) => {
console.log(error.code)
console.log(error.message)
console.log(error.email)
})
}
render() {
return (
<Button onClick={this.signup} variant="contained" className="btn-google">
Sign Up with Google
<img className='imgGoogle' alt={"google-logo"} src={require("../../assets/img/search.png")} />
</Button>
);
}
}
const mapStateToProps = state => {
return {
user: state.user
};
}
const mapDispatchToProps = dispatch => {
return {
logUser: (user) => dispatch({type: LOG_USER, user: user})
};
}
export default connect(mapStateToProps, mapDispatchToProps)(SignupGoogle);
And my index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
import { BrowserRouter as Router } from 'react-router-dom';
import { Provider } from 'react-redux'
import { createStore } from 'redux';
import reducers from './reducers/reducers';
const store = createStore(reducers)
ReactDOM.render(
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>,
document.getElementById('root')
);
serviceWorker.unregister();
This is what I can get at my browser log after login with Google firebase:
That's because you're onClick handler method is not bound to the instance of the component, modify your constructor like this and your props should no longer return undefined:
constructor(props) {
super(props);
this.signup = this.signup.bind(this);
}
Alternatively you could also modify your onClick method to look like this:
<Button onClick={() => this.signup()} variant="contained" className="btn-google">
or turn your onClick handler method into an arrow function:
signup = () => {
// ...
}
...
<Button onClick={this.signup} variant="contained" className="btn-google">
but the first option using bind is the preferred one.
Refer to the docs for more information on event handling.
EDIT:
I missed that there was another callback function involved.
You're accessing this.props from within another function in the signInWithPopup-callback. Change your callback to an arrow function, which should preserve the context of the signup method and fix your issue:
firebase.auth().signInWithPopup(provider).then(result => {
// ...
}).catch(error => {
// ...
});
It's all about context. Since your signup function is bound to the onclick event, the this context is the <button>.
You can either in the constructor set the this context:
constructor(props) {
super(props);
this.signup = this.signup.bind(this);
}
or use arrow syntax:
signup = () => {
}
React documentation has a good answer for event binding here: https://reactjs.org/docs/handling-events.html
Your signup definition is fine, but you can just wrap it in an arrow function that has the proper 'this' value.
onClick={()=>signup()}

How to not overwrite dispatch with blank state

Background:
I am practicing the idea of React/Redux. I would want to follow the flow of data.
axios dispatches action -> reducer setState to props -> Component render()
The problem may be more than 1 point. Because I am new to Frontend world.
Please feel free to re-design my app(if needed)
Problem:
company does not render out because this.props.companies is blank. But axios does fetch the array from backend.
action/index.js
//First experiment action returns promise instance
export function fetchCompanies(token) {
const jwtReady = 'JWT '.concat(token);
const headers = {
'Content-Type': 'application/json',
'Authorization': jwtReady
};
const instance = axios({
method: 'GET',
url: `${ROOT_URL}/api/companies/`,
headers: headers
});
return {
type: FETCH_COMPANIES,
payload: instance
}
}
export function getCompanies(token){
const jwtReady = 'JWT '.concat(token);
const headers = {
'Content-Type': 'application/json',
'Authorization': jwtReady
};
const instance = axios({
method: 'GET',
url: `${ROOT_URL}/api/companies/`,
headers: headers
});
return instance
.then(data=> store.dispatch('GET_COMPANIES_SUCCESS', data));
}
company_reducers.js
import {FETCH_COMPANIES, GET_COMPANIES_ERROR, GET_COMPANIES_SUCCESS} from "../actions/const";
export default function (state = {}, action) {
switch (action.type) {
case GET_COMPANIES_SUCCESS:
return {
...state,
companies: action.payload
};
case GET_COMPANIES_ERROR:
return {
...state,
err_msg: action.payload.text
};
default:
return state;
}
}
reducers/index.js
import {combineReducers} from 'redux';
import {reducer as formReducer} from 'redux-form';
import LoginReducer from './login_reducers';
import CompanyReducer from './company_reducers';
const rootReducer = combineReducers({
login: LoginReducer,
companies: CompanyReducer,
form: formReducer
});
export default rootReducer;
component/select_teams.js
import _ from 'lodash';
import React, {Component} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import {fetchCompanies, getCompanies} from "../actions";
import {Link} from 'react-router-dom';
class SelectTeam extends Component {
constructor(props) {
super(props);
const token = localStorage.getItem('token');
this.state = {
token,
companies: null,
err_msg: null
}
}
componentWillMount() {
const tmp = this.props.getCompanies(this.state.token);
tmp.then(res => {
console.log(res)
})
.catch(err => {
console.log(err);
})
};
renderErrors() {
return (
<div>{this.state.err_msg}</div>
);
}
renderCompanies() {
return _.map(this.props.companies, company => {
return (
<li className="list-group-item" key={company.id}>
<Link to={`/${company.id}`}>
{company.name}
</Link>
</li>
)
});
}
render() {
if (this.props.companies === null) {
return (
<div>Loading...</div>
);
}
console.log(this.props);
return (
<div>
<h3>❤ Select Team ❤</h3>
{this.renderErrors()}
{this.renderCompanies()}
</div>
);
}
}
function mapStateToProps(state){
return {companies: state.companies}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
fetchCompanies: fetchCompanies,
getCompanies: getCompanies
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(SelectTeam);
App.js
import React, {Component} from 'react';
import './App.css';
import SelectTeam from "./components/select_teams";
import reducers from './reducers/index';
import {Provider} from 'react-redux';
import promise from "redux-promise";
import {applyMiddleware, createStore} from 'redux';
import {BrowserRouter, Route, Switch, Redirect} from 'react-router-dom';
import LoginPage from './components/loginPage';
const createStoreWithMiddleware = applyMiddleware(promise)(createStore);
const PrivateRoute = ({component: Component, isAuthorized, ...otherProps}) => (
<Route
{...otherProps}
render={props => (
isAuthorized() ? (<Component {...props} />) :
(
<Redirect to={
{
pathname: '/login',
state: {from: props.location},
}
}
/>
)
)}
/>
);
function PageNotFound() {
return (
<div>404 Page Not Found</div>
);
}
// TODO: I will add RESTful validation with backend later
function hasToken() {
const token = localStorage.getItem('token');
const isAuthenticated = !((token === undefined) | (token === null));
return isAuthenticated;
}
export const store = createStoreWithMiddleware(reducers);
class App extends Component {
//I will add security logic with last known location later.
//Get the features done first
render() {
return (
<Provider store={store}>
<BrowserRouter>
<div>
<Switch>
<PrivateRoute exact path="/select-teams" isAuthorized={hasToken} component={SelectTeam}/>
<Route path="/login" component={LoginPage}/>
<Route component={PageNotFound}/>
</Switch>
</div>
</BrowserRouter>
</Provider>
);
}
}
export default App;
You should dispatch an action with the data fetched from the server.
Actions are pure functions that return an object (the object has at minimum a TYPE field).
If you have any async operations, you may use Redux-Thunk, which is an action creator that returns a function, and call the api fetch within it.
Here is the actions snippet:
// imports..
export const fetchCompaniesSuccess = (data) => {
retyrn {
type: FETCH_COMPANIES_SUCCESS,
data
}
}
export const fetchCompanies = (token) => dispatch => {
// ...
axios(...).then(dispatch(data => fetchCompaniesSuccess(data)))
}
In your company_reducers.js,
// Company Reducer Function, State here represents only the companies part of the store
case FETCH_COMPANIES_SUCCESS: // should match the the type returned by the action
return [
...state,
...action.data
]
// other cases & default
MAKE SURE to add redux-thunk as a middleware in your createStore, read Redux-Thunk doc for instructions.
then in you component:
componentDidMount(){
this.props.fetchCompanies(this.state.token);
}
Once companies data is added to the redux store, your component will rerender and the companies array will be available in props
You don't need to have a duplicate companies array in the component state.
You may want to Watch Dan Abramov introduction to redux, it is a free course.
Seems like your dispatch syntax is wrong. The parameter should be an object with type and payload.
return instance
.then(data=> store.dispatch({
type: 'GET_COMPANIES_SUCCESS',
payload: data
}));

Mobx store not injected

i try do that Store objet
UserStore.js
import { observable, action } from 'mobx';
class UserStore {
constructor() {
const me = observable({
me: null,
auth: action.bound(function(me) {
this.me = me;
})
})
}
}
export default UserStore;
After this, i do that
App.js
const App = inject('routing','UserStore')(observer(class App extends Component {
constructor(props, context) {
super(props, context);
this.handleFiles = this.handleFiles.bind(this);
this.prepareTable = this.prepareTable.bind(this);
this.state = {excel: null};
}
render() {
const {location, push, goBack} = this.props.routing;
const {userStore} = this.props.userStore;
And in index.js i do
const stores = {
// Key can be whatever you want
routing: routingStore,
UserStores
};
const history = syncHistoryWithStore(browserHistory, routingStore);
ReactDOM.render(
<Provider {...stores}>
<Router history={history}>
<Entry/>
</Router>
</Provider>,
document.getElementById('root')
);
Lets, after all of this i try open the localhost:3000 and se this error
Error: MobX observer: Store 'UserStore' is not available! Make sure it is provided by some Provider
UPDATE:
I'm create a project with create-react-app, and i can't use # in code(example for #injector)
I think you should return an instance of the store.
To be a bit more organized I have a file like "storeInitializer.ts" (using typescript here):
import YourStoreName from '../yourStoreFolder/yourStore';
export default function initializeStores() {
return {
yourStoreName: new YourStoreName(),
}
}
Then I have another file like "storeIdentifier.ts":
export default class Stores {
static YourStoreName: string = 'yourStoreName';
}
In the app file I do something like this:
import React from 'react';
import { inject, observer } from 'mobx-react';
import Stores from './storeIdentifier';
const App = inject(Stores.YourStoreName)(observer(props: any) => {
//code here
});
export default App;
or if you are using the class component approach...
import React from 'react';
import { inject, observer } from 'mobx-react';
#inject(Stores.YourStoreName)
#observable
class App extends Component {
//code here
}
export default App;

TypeError: Cannot read property 'pathname' of undefined in react/redux testing

I'm testing some react components, a basic tests suite just to know if a component is rendering and their childs.
I'm using redux-mock-store to make the store and {mount} enzyme to mount the container in a provider, but even mocking the correct store this error is always fired:
TypeError: Cannot read property 'pathname' of undefined
Here is my very deadly basic test:
import React from 'react';
import { mount } from 'enzyme';
import configureStore from 'redux-mock-store';
import { Provider } from 'react-redux';
import App from '../containers/App.container';
describe('App', () => {
let wrapper;
const mockStore = configureStore([]);
const store = mockStore({
router: {
location: { pathname: '/home', query: {}, search: '' },
params: {}
}
});
console.log(store.getState());
beforeEach(() => {
wrapper = mount(
<Provider store={store}>
<App />
</Provider>
);
});
it('Should render app and container elements', () => {
expect(wrapper.find('.app').exists()).toBeTruthy();
expect(wrapper.find('.container').exists()).toBeTruthy();
});
it('Should render the navbar', () => {
expect(wrapper.find('nav').exists()).toBeTruthy();
});
});
And the (even more) simple component / container:
import React, { Component } from 'react';
import NavBar from '../components/Navbar';
class App extends Component {
render() {
const { location, logout} = this.props;
console.log(location);
return (
<section className='app'>
<NavBar location={location.pathname} onLogoutClick={logout}/>
<div className='container'>
{this.props.children}
</div>
</section>
);
}
}
export default App;
Container:
import { connect } from 'react-redux';
import { signOut } from '../actions/auth.actions'
import App from '../components/App';
const mapStateToProps = (state, ownProps) => {
return {
location: ownProps.location
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
logout: () => {
dispatch(signOut())
}
}
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
I can't figure out the problem of the test, the mockStore is in the correct format:
Any idea?
Update:
Thinking about it, I have no reducer / prop in the rootReducer for the location, but, i just want to pass down through the children components the location object properties that react-redux-router make available in the ownProps argument.
Weird fact: logging the location property in the app returns me the correct object.
In the tests, is always undefined... (as the error shows).
Here is my rootReducer:
import { combineReducers } from 'redux';
import { reducer as formReducer } from 'redux-form';
import { routerReducer } from 'react-router-redux';
import authReducer from './auth.reducer';
import analysisReportsReducer from './AnalysisReports.reducer';
import titleAnalysisReducer from './TitleAnalysis.reducer';
import postsReportsReducer from './PostsReports.reducer';
const rootReducer = combineReducers({
form: formReducer,
routing: routerReducer,
auth: authReducer,
analysis: titleAnalysisReducer,
analysis_reports: analysisReportsReducer,
posts: postsReportsReducer
});
export default rootReducer;
It looks like your location object is scoped beneath the router.
Your test may be grabbing the window.location property, which your test suite may not replicate, assuming the test is cli and not in a browser.
Perhaps try:
<NavBar location={this.props.router.location.pathname} onLogoutClick={logout}/>

electron-react-boilerplate - Route change and state

I am using electron-react-boilerplate to start a small application for task tracking using NeDB for persistance.
When I start my application, for first time, and change first route,
##router/LOCATION_CHANGE is fired but my state is empty, and then after that action (LOAD_NOTES -action that I defined) is fired (loading my data - server request) - First time I have small error flash (undefined variable - my state is empty because data is getting loaded in LOAD_NOTES_REQUEST action which "fires" LOAD_NOTES actions after loading of data is finished).
Any idea why is that happening - how to populate my state at route change in proper way ?
EDIT
actions/project.js
import * as ActionTypes from '../constants/ActionTypes';
import * as axios from 'axios';
import Alert from 'react-s-alert';
const baseURL = 'http://localhost:3000';
export function addProject(project) {
return {
type: ActionTypes.ADD_PROJECT,
project
};
}
export function addProjectRequest(project) {
return dispatch => {
dispatch(addProject(project));
axios.post(`${baseURL}/api/addProject`, project)
.then(function (response) {
Alert.success('Test message 3', {
position: 'top-right',
});
})
.catch(function (response) {
console.log(response);
});
};
}
reducers/projectReducer.js
import * as ActionTypes from '../constants/ActionTypes';
const initialState = { projects: [], project:null};
const projectsReducer = (state = initialState, action) => {
switch (action.type) {
case ActionTypes.ADD_PROJECT :
return {
projects: [{
projectName: action.event.projectName,
projectWorkOrder: action.event.projectWorkOrder,
projectClient: action.event.projectClient,
projectDescription: action.event.projectDescription,
projectStatus: action.event.projectStatus,
_id: action.event._id,
}, ...state.projects],
project: state.project
};
case ActionTypes.ADD_PROJECTS :
return {
projects: action.projects,
project: state.project,
};
case ActionTypes.GET_PROJECT :
return {
projects: state.projects,
project: action.project
};
default:
return state;
}
};
export default projectsReducer;
containers/projectDetailContainer.js
import React, { PropTypes, Component } from 'react';
import { bindActionCreators } from 'redux';
import ProjectDetail from '../components/projects/ProjectDetail';
import { connect } from 'react-redux';
import * as ProjectActions from '../actions/project';
class ProjectDetailContainer extends Component {
constructor(props, context) {
super(props, context);
}
componentDidMount() {
this.props.dispatch(ProjectActions.getProjectRequest(this.props.params.id));
}
render() {
return (
<div>
<ProjectDetail singleProject={this.props.project} />
</div>
);
}
}
ProjectDetailContainer.need = [(params) => {
return Actions.getProjectRequest.bind(null, params.id)();
}];
ProjectDetailContainer.contextTypes = {
router: React.PropTypes.object,
};
function mapStateToProps(store) {
return {
project: (store.projectsReducer.project)
};
}
export default connect(mapStateToProps)(ProjectDetailContainer);
components/ProjectDetail.js
import React, { Component } from 'react';
export default class ProjectDetail extends Component {
constructor(props, context) {
super(props, context);
}
render() {
return (
<div>
<h1>Project details</h1>
<div>{(this.props.singleProject[0].projectName)}</div>
</div>
);
};
}
routes.js
import React from 'react';
import { Route, IndexRoute } from 'react-router';
import App from './containers/App';
import HomePage from './containers/HomePage';
import CounterPage from './containers/CounterPage';
import Dashboard from './containers/Dashboard';
import NewProjectContainer from './containers/NewProjectContainer';
import ProjectsListContainer from './containers/ProjectsListContainer';
import ProjectDetailContainer from './containers/ProjectDetailContainer';
export default (
<Route path="/" component={App}>
<IndexRoute component={Dashboard} />
<Route path="/counter" component={CounterPage} />
<Route path="/new-project" component={NewProjectContainer} />
<Route path="/projects-list" component={ProjectsListContainer} />
<Route path="/projects/:id" component={ProjectDetailContainer} />
</Route>
);
After going through your question and the code I can see that the key part of your question is:
Any idea why is that happening - how to populate my state at route change in proper way ?
Well, you may need to play with component in your routes.js. This React Router main documentation will help: https://github.com/ReactTraining/react-router/blob/master/docs/API.md#route
Further more, following may also assist in case if you are looking for something else:
fetching data before changing route with react-router
redux store state between page route

Categories

Resources