I'm just starting to learn redux with react and I've been having this simple issue for a while now that I can't figure out. I have a button that dispatches an action to increment a value, and the action goes through and updates the state, but no change is ever reflected in the component. What am I doing wrong here?
const ACTION = 'ACTION';
const defaultState = {
value: 5
};
const doAction = ()=>{
return {
type: ACTION
};
};
const reducer = (state = defaultState, action) => {
let update = Object.assign({}, state);
switch(action.type){
case ACTION:
update.value = state.value + 1;
return update;
default:
return state;
}
};
const store = Redux.createStore(reducer);
class App extends React.Component {
constructor(props){
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e){
this.props.doAction();
}
render(){
return(
<div id='app'>
<button onClick={this.handleClick}>Click</button>
<p>{this.props.value}</p>
</div>
);
}
}
const Provider = ReactRedux.Provider;
const mapStateToProps = (state)=> {
return {
value: state.value
};
}
const mapDispatchToProps = (dispatch) => {
return {
doAction: () => {
dispatch(doAction())
}
};
}
const Container = ReactRedux.connect(mapStateToProps, mapDispatchToProps)(App);
class AppWrapper extends React.Component {
constructor(props){
super(props);
}
render(){
return(
<Provider store={store}>
<Container />
</Provider>
);
}
}
ReactDOM.render(<AppWrapper />, document.getElementById('root'));
Issue regarding codepen.io
Codepen provides the newest version of react-redux by default, unluckily it is a test version (5.1.0-test.1) currently. Change the external script url of react-redux to https://cdnjs.cloudflare.com/ajax/libs/react-redux/5.0.7/react-redux.js and your pen should be working: Working example
I don't recommend codepen for react development. Consider create-react-app or codesandbox.io if it has to be an online ide. You really need more than one editable file to structure your actions, reducers and components.
How to improve your code
In your reducer you create a copy of the previous state in every case, even when you return the previous state. Try this instead:
const reducer = (state = initialState, action) => {
switch (action.type) {
case ACTION:
return { ...state, value: state.value + 1 };
// Alternative: Object.assign({}, state, { value: state.value + 1 });
default:
return state;
}
};
This will ensure you create objects only if needed. You could have returned { value: state.value + 1 } for this simple state shape, the spread operator or Object.assign(...) are a valuable tool when you want to clone the previous state and change only some (or delete) properties without affecting others.
Furthermore you can refactor your app to a functional react component:
const App = (props) => (
<div>
<button onClick={props.action}>Click</button>
<p>{props.value}</p>
</div>
);
And connect the App to redux with:
const mapStateToProps = (state) => ({
value: state.value
});
const mapDispatchToProps = dispatch => ({
action: () => dispatch(doAction())
});
const Container = connect(mapStateToProps, mapDispatchToProps)(App);
Note that i stick to your component names here.
Where to go from here...
If you are interested in a bigger example i have created this sandbox which features routing, fake authentication, a todo list and a simple counter. Redux devtools is integrated, too. Just open the browser extension on this site to inspect the store and the dispatched actions.
Related
EDIT: I think the issue is with my reducers
I only have one reducer called "filmsReducer" where I do this at the end :
export default combineReducers({
films: filmsReducer
});
I'm doing an app in React Native using Redux,
I want to get the initialState values below in a component :
const initialState = {
name: "",
likedFilms: [299534, 49530, 629],
dislikedFilms: [100241, 559969]
};
const filmsReducer = (state = initialState, action) => {
const { likedFilms, dislikedFilms } = state;
switch (action.type) {
case ADD_FILM:
if (action.array === "like") {
const newLikedFilms = [...state.likedFilms, action.payload];
return {
...state,
likedFilms: newLikedFilms
};
} else {
const newDislikedFilms = [...state.dislikedFilms, action.payload];
return {
...state,
dislikedFilms: newDislikedFilms
};
}
default:
return state;
}
};
And here's the component, I want to get likedFilms array from the redux state in the props of this component, but the console log doesn't work :
class LikedScreen extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.log(this.props.likedFilms); <-- doesn't work
}
}
const mapStateToProps = state => ({
likedFilms: state.likedFilms
});
export default connect(mapStateToProps)(LikedScreen);
Regarding your comment, you probably have to adapt your code to the following:
Edit Regarding another comment of yours, you need to change it to films instead of FilmsReducer:
const mapStateToProps = state => ({
likedFilms: state.films.likedFilms
});
It will be like, use reducer name as while mapping props in Component
class LikedScreen extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.log(this.props.likedFilms); <-- doesn't work
}
}
const mapStateToProps = state => ({
likedFilms: state.films.likedFilms
});
export default connect(mapStateToProps)(LikedScreen);
I am new in redux.
My code :
Home Screen
<Text> {{this.props.mycity}} </Text>
const mapStateToProps = function(state) {
return {
mycity: state.layersFlag.baseDistrictADhabi //consist true/false
}
}
export default connect(mapStateToProps)(HomeScreen);
Sidemenu Screen :
UI
<Switch onValueChange={(flag) => {
this.props.toggleCity();
} value={this.state.city} />
const mapDispatchToProps = dispatch => {
return {
toggleCity: () => {
dispatch({ type: "changeCity" })
}
};
};
export default connect(null, mapDispatchToProps)(SideMenuScreen);
Store and reducer setup :
const initialState = {
city : {
mycity: true
}
};
const reducer = (state = initialState, action)=>{
switch (action.type) {
case "changeCity":
return Object.assign({}, state, {
mycity: action.payload.mycity
})
default:
return state;
}
}
const Store = createStore(reducer);
I am stuck in sidemenu. How to dispach in mapDispatchToProps method:
How to pass action in mapDispatchToProps in sidemenu?
If my assumptions on what your Switch component does is correct, it would trigger the onValueChange event-listener when you pass in this.state.city to the value prop. You end up calling this.props.toggleCity() to dispatch your changeCity action. I think the set-up is correct for here...
However, it looks like your reducer is expecting an action.payload which you never passed in as part of the action.
const reducer = (state = initialState, action)=>{
switch (action.type) {
case "changeCity":
return Object.assign({}, state, {
mycity: action.payload.mycity
})
default:
return state;
}
}
So yes the dispatch is working correctly, but you are not passing all the necessary data for your reducer to return a new piece of state.
You need to update your mapDispatchToProps, your event-handler and your reducer to something like
<Switch onValueChange={(flag) => {
this.props.toggleCity(this.state.city);
} value={this.state.city} />
const mapDispatchToProps = dispatch => {
return {
toggleCity: (myCity) => {
dispatch({ type: "changeCity", payload: myCity })
}
};
};
export default connect(null, mapDispatchToProps)(SideMenuScreen);
Your reducer also seems to have an extra key, you don't need to access the mycity prop in payload if its already the payload. Update to:
const reducer = (state = initialState, action)=>{
switch (action.type) {
case "changeCity":
return Object.assign({}, state, {
mycity: action.payload
})
default:
return state;
}
}
Adding on, if you want your Hone component to re-render with the new data in your redux-state, you can do something like this.
In your HomeScreen component, make use of a state-variable to save your abudhabi or whatever city-value and call componentDidUpdate() to setState and re-render your component.
class HomeScreen extends React.Component{
state = {
abudhabi: false
}
//when the component gets the new redux state this will trigger
componentDidUpdate(prevProps){
if(this.props.abudhabi !== prevProps.abudhabi){
this.setState({
abudhabi: this.props.abudhabi
})
}
}
}
I am trying to implement auth (sign up/out) using React + Redux (SSR and Thunks). I have no idea why components are not updatating when Redux state updates...
This is the component that should get rerendered:
class Navbar extends React.Component {
constructor(props) {
super(props);
this.state = {
loggedIn: props.authentication.loggedIn
};
}
render() {
let links = null;
if (this.state.loggedIn) {
links = ...
} else {
links = ...
}
return (<Toolbar>{links}</Toolbar>)
}
}
const mapStateToProps = state => {
return {
authentication: state.authentication
}
}
const mapDispatchToProps = dispatch => {
return {
signOut: () => {dispatch(userActions.logout())}
}
}
const AuthNavbar = connect(mapStateToProps, mapDispatchToProps)(Navbar)
export default AuthNavbar;
And that is my reducer:
const reducers = {
authentication,
registration,
alert
}
const todoApp = combineReducers(reducers)
export default todoApp
Authentication reducer:
const authentication = (state = initialState, action) => {
switch (action.type) {
...
case userConstants.LOGIN_SUCCESS:
return Object.assign({}, state, {
loggedIn: true,
loggingIn: false,
user: action.user
});
...
default:
return state;
}
}
And The Action - Login:
function login(email, password) {
return dispatch => {
dispatch({type: userConstants.LOGIN_REQUEST, email});
userService.login(email, password).then(
user => {
dispatch({type: userConstants.LOGIN_SUCCESS, user});
},
error => {
dispatch({ type: userConstants.LOGIN_FAILURE });
dispatch({type: alertActions.error(error)});
}
);
}
}
UserService.login is a function that calls and api witch fetch.
Looks like Action gets fired as it should, Redux state gets updated, but the component does not update:
Double checked Redux Dev Tools - state does get updated, so there must be a problem with the way I am using connect utilities?
You are storing the logedin props in the state inside the constructor, which will run only once in the life time of the component.
When a new prop is coming back you are not updating the state.
Either use the props directly:
if (this.props.authentication.loggedIn) {
links = ...
Or update the state in componentWillReceiveProps
componentWillReceiveProps(nextProps){
// update the state with the new props
this.setState({
loggedIn: nextProps.authentication.loggedIn
});
}
Your render function is dependent on state.loggedIn, but state.loggedIn is not changing; only this.props.authentication.loggedIn is changing in response to the action. Your component, in its current form, does not need state. You can remove it to make this work:
class Navbar extends React.Component {
render() {
let links = null;
if (this.props.authentication.loggedIn) {
links = ...
} else {
links = ...
}
return (<Toolbar>{links}</Toolbar>)
}
}
Using the terminal to test my dispatched actions, Redux-logger shows that my state is being correctly updated. However, my component is not re-rendering as a result of the state change. I've looked at the SO answers regarding component not re-rendering, a majority of the responses claim that the state is being mutated; as a result, Redux won't re-render. However, I'm using Lodash's merge to deep-dup an object, I'm pretty sure I'm not returning a modified object. (Please see attached snippet below)
Would love to hear some advice from you guys, pulling my hair out on this one!
const usersReducer = (state = {}, action) => {
Object.freeze(state); // avoid mutating state
console.log(state);
// returns an empty object
let newState = merge({}, state);
console.log(newState);
// returns my state with my dispatched action object inside already???
// newState for some reason already has new dispatched action
switch (action.type) {
case RECEIVE_USER:
let newUser = {[action.user.id] = action.user};
return merge(newUser, newUser);
case RECEIVE_USERS:
newState = {};
action.users.forEach(user => {
newState[user.id] = user;
});
return merge({}, newState);
default:
return state;
}
};
React Container Component
import { connect } from 'react-redux';
import { receiveUsers, receiveUser, refreshAll, requestUsers, requestUser } from '../../actions/user_actions';
import allUsers from '../../reducers/selectors';
import UserList from './user_list';
const mapStateToProps = (state) => ({
users: allUsers(state), // allUsers (selector that takes the state specfically the user Object and returns an array of user Objects)
state
});
const mapDispatchToProps = (dispatch) => ({
requestUser: () => dispatch(requestUser()),
requestUsers: () => dispatch(requestUsers()),
receiveUsers: (users) => dispatch(receiveUsers(users)),
receiveUser: (user) => dispatch(receiveUser(user)),
refreshAll: (users) => dispatch(refreshAll(users))
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(UserList);
React Presentational component
import React from 'react';
class UserList extends React.Component {
render() {
const { users, state } = this.props;
const userItems = users.map((user, idx) => {
return(<li key={idx}>{user.username}</li>);
});
return (
<div>
<ul>
{ userItems }
</ul>
</div>
);
}
}
export default UserList;
React Store
import { createStore, applyMiddleware } from 'redux';
import createLogger from 'redux-logger';
import RootReducer from '../reducers/root_reducer';
const logger = createLogger();
const configureStore = (preloadedState = {}) => {
return createStore(
RootReducer,
preloadedState,
applyMiddleware(logger));
};
// const configureStore = createStore(rootReducer, applyMiddleware(logger));
// oddly enough, when I have the store as a constant and not a function that returns the store constant, dispatching actions through the terminal will correctly update the state and rerender the component
export default configureStore;
React Selector
const allUsers = ({ users }) => {
return Object.keys(users).map(id => (
users[id]
));
};
export default allUsers;
I had a similar problem, just in case someone stumbles upon this, I needed to clone the array in order to re-render the view:
export const addFieldRow = () => (
(dispatch: any, getState: any) => {
const state = getState();
const myArrayOfObjects = myArrayOfObjectsProp(state);
const newObject = {
key: "",
value: "",
};
myArrayOfObjects.push(newObject);
dispatch(addFieldRowAction({ myArrayOfObjects: [...myArrayOfObjects] })); <== here
}
);
Common problem in this case is using not reactive operations for changing state. For example use concat() for array, but not push() and so on.
I use this solution to do it.
I put the users on my state and update it on any change, with componentWillReceiveProps. Hope it helps :-)
class UserList extends React.Component {
constructor(props) {
super(props);
console.log(this.props);
this.state = {
users: props.users
};
}
componentWillReceiveProps(nextProps) {
if (this.props.users !== nextProps.users) {
this.setState({
users: nextProps.users,
});
}
}
render() {
const { users } = this.state;
const userItems = users.map((user, idx) => {
return(<li key={idx}>{user.username}</li>);
});
return (
<div>
<ul>
{ userItems }
</ul>
</div>
);
}
}
export default UserList;
What do your React components look like? Are you using internal state in them or props to push the data down. Usually I see the issue is with people setting the internal state of props with Redux state. You should be pushing props down to the components and they will re-render on update.
Also, check out
https://chrome.google.com/webstore/detail/react-developer-tools/fmkadmapgofadopljbjfkapdkoienihi?hl=en
to see if the props are really changing.
Create new copy of array from the prop state to re-render the component
render() {
const {allPost} = this.props;
//Use the spread operator to create a new copy of the array
const posts = [...allPost];
const plansList = () => {
return posts.length < 1 ? null : posts && <PlansList allPost={posts}
/>;
};
return (
<>
<Container className="mt-lg-5 pt-lg-5">
{plansList()}
</Container>
</>
);
}
i spent lot of time to find out that when using more than 1 reducer (with combineReducers), then your mapStateToProps should point the correct reducer names, for example
const mapStateToProps = state => ({
someVar: state.yourReducerName.someVar,
loading: state.yourReducerName.loading,
error: state.yourReducerName.error
});
I am new to react native + redux. I have an react native application where user first screen is login and after login am showing page of list of categories from server. To fetch list of categories need to pass authentication token, which we gets from login screen or either if he logged in previously then from AsyncStorage.
So before redering any component, I am creating store and manully dispatching fetchProfile() Action like this.
const store = createStore(reducer);
store.dispatch(fetchProfile());
So fetchProfile() try to reads profile data from AsyncStorage and dispatch action with data.
export function fetchProfile() {
return dispatch => {
AsyncStorage.getItem('#myapp:profile')
.then((profileString) => {
dispatch({
type: 'FETCH_PROFILE',
profile: profileString ? JSON.parse(profileString) : {}
})
})
}
}
so before store get populated, login page get rendered. So using react-redux's connect method I am subscribing to store changes and loading login page conditionally.
class MyApp extends React.Component {
render() {
if(this.props.profile)
if(this.props.profile.authentication_token)
retunr (<Home />);
else
return (<Login />);
else
return (<Loading />);
}
}
import { connect } from 'react-redux';
const mapStateToProps = (state) => {
return {
profile: state.profile
}
}
module.exports = connect(mapStateToProps, null)(MyApp);
So first 'Loading' component get rendered and when store is populated then either 'Login' or 'Home' component get rendered. So is it a correct flow? Or is there a way where I can get store populated first before any compnent render and instead of rendering 'Loading' component I can directly render 'Login' or 'Home' Component.
Verry common approach is to have 3 actions for an async operation
types.js
export const FETCH_PROFILE_REQUEST = 'FETCH_PROFILE_REQUEST';
export const FETCH_PROFILE_SUCCESS = 'FETCH_PROFILE_SUCCESS';
export const FETCH_PROFILE_FAIL = 'FETCH_PROFILE_FAIL';
actions.js
import * as types from './types';
export function fetchProfile() {
return dispatch => {
dispatch({
type: types.FETCH_PROFILE_REQUEST
});
AsyncStorage.getItem('#myapp:profile')
.then((profileString) => {
dispatch({
type: types.FETCH_PROFILE_SUCCESS,
data: profileString ? JSON.parse(profileString) : {}
});
})
.catch(error => {
dispatch({
type: types.FETCH_PROFILE_ERROR,
error
});
});
};
}
reducer.js
import {combineReducers} from 'redux';
import * as types from './types';
const isFetching = (state = false, action) => {
switch (action.type) {
case types.FETCH_PROFILE_REQUEST:
return true;
case types.FETCH_PROFILE_SUCCESS:
case types.FETCH_PROFILE_FAIL:
return false;
default:
return state;
}
};
const data = (state = {}, action) => {
switch (action.type) {
case types.FETCH_PROFILE_SUCCESS:
return action.data;
}
return state;
};
export default combineReducers({
isFetching,
data
});
So you can get isFetching prop in your component and show/hide Loader component
You can load all your data during the splash screen and then load the others screens after that. I did it like this. Hope it helps
class Root extends Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
store: configureStore( async () => {
const user = this.state.store.getState().user || null;
if (categories && categories.list.length < 1) {
this.state.store.dispatch(categoriesAction());
}
this.setState({
isLoading: false
});
}, initialState)
};
}
render() {
if (this.state.isLoading) {
return <SplashScreen/>;
}
return (
<Provider store={this.state.store}>
<AppWithNavigationState />
</Provider>
);
}
}
Redux and Redux Persist (https://github.com/rt2zz/redux-persist) will solve your problem.
Don't make them complex.