I'm in the process of learning React Native with Redux and can't wrap my head around how to access the store/actionCreators from within my components. I feel like I've tried ten or so variants of code, watched most of Dan Abramov's Egghead videos (multiple times), and still don't understand what I'm doing wrong. I think the best explanation I've seen so far is the accepted answer here: Redux, Do I have to import store in all my containers if I want to have access to the data?
The error I get is: undefined is not an object (evaluating 'state.clinic.drName'). Here are the relevant bits of code:
I'm passing the store via the Provider (so I think):
index.ios.js
import clinicReducer from './reducers/clinic';
import { createStore, combineReducers } from 'redux';
import { Provider } from 'react-redux';
const rootReducer = combineReducers({
clinic : clinicReducer
});
let store = createStore(combineReducers({clinicReducer}));
//testing - log every action out
let unsubscribe = store.subscribe( () =>
console.log(store.getState())
);
class ReactShoeApp extends React.Component {
render() {
return (
<Provider store={store}>
<ReactNative.NavigatorIOS
style={styles.container}
initialRoute={{
title: 'React Shoe App',
component: Index
}}/>
</Provider>
);
}
}
Here is my actionCreator:
export const CLINIC_DR_NAME_UPDATE = 'CLINIC_DR_NAME_UPDATE_UPDATE';
export function updateClinicDrName(newValue) {
return {type: CLINIC_DR_NAME_UPDATE, value: newValue};
}
Here is my reducer:
import {
CLINIC_DR_NAME_UPDATE
} from '../actions/';
let cloneObject = function(obj) {
return JSON.parse(JSON.stringify(obj));
};
const initialState = {
drName : null
};
export default function clinicReducer(state = initialState, action) {
switch (action.type) {
case CLINIC_DR_NAME_UPDATE:
return {
...state,
drName: action.value
};
default:
return state || newState;
}
}
and here is my component:
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as Actions from '../actions';
const mapStateToProps = function(state){
return {
drName: state.clinic.drName,
}
};
const mapDispatchToProps = function (dispatch) {
return bindActionCreators({
updateClinicDrName: Actions.updateClinicDrName,
}, dispatch)
};
//class definition
class Clinic extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<View style={styles.rowContainer}>
<View style={styles.row}>
<TextGroup label={'Dr. Name'} placeholder={'placeholder'} value={this.props.drName} onChangeText={(text) => this.handlers.onNameChange({text})}></TextGroup>
<TextGroup label={'City'} placeholder={'placeholder'}></TextGroup>
</View>
</View>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Clinic);
All I'm trying to do is update the store (clinic.drName) when the user types in the Dr Name Text Group. Any and all help is appreciated.
The issue is you're mismatching the way you call combineReducers() with the name of the slice you're expecting. This is a common mistake. You're defining an object with the key clinicReducer, but expecting the data to be at a key named clinic.
See Defining State Shape for an explanation of the problem.
Also, your clinicReducer() function looks odd, and has several issues:
Don't access variables outside the function like you are with newState.
Don't do deep-cloning like that (per Redux FAQ: Performance). Instead, use "shallow cloning" to update the state if necessary (see examples at Immutable Update Patterns).
It looks like the reducer is double-nesting the data it's trying to access as well.
I think you want something more like:
// clinicReducer.js
const initialState = {
drName : null
};
export default function clinicReducer(state = initialState, action) {
switch(action.type) {
case CLINIC_DR_NAME_UPDATE: {
return {
...state,
drName : actino.value
};
}
default: return state
}
}
// index.js
import clinicReducer from "./reducers/clinic";
const rootReducer = combineReducers({
clinic : clinicReducer
});
What errors are you getting? I see two issues with what you have.
newState.clinic.drName = action.value; should be throwing a typeError. So for this to work you need to change newState to:
const newState = {
clinic: {}
}
What is this.handlers.onNameChange({text})? I think you need to change this to this.props.updateClinicDrName(text).
Related
I'm having trouble figuring out why my React Native component isn't preforming dispatching any of the actions I've tried to connected to it. I believe I've correctly followed the suggested approach to defining matchDispatchToProps as an object, but none of the expected actions seem to be happening.
Everything works fine if I explicitly import store. For example
store.dispatch({type: 'INCREMENT'})
works where the examples using just
increment
fails.
How do I correctly dispatch actions using mapDispatchToProps in place of directly accessing the Redux store?
In fact, I wonder why I would't just add something like
export const counterAPI = bindActionCreators(
{ increment, reset },
store.dispatch
)
in my store.ts (no longer exporting anything else from there, except store for use by Provider) and change
import { increment, reset } from "../store"
// ...
export default connect(null, mapDispatchToProps)(DemoCounter)
in DemoCounter.tsx to just
import { counterAPI } from "../store"
// ...
export default connect(null)(DemoCounter)
That seems to be a lot simpler and to achieve exactly the right level of modularity.
DemoCounter.tsx:
import React, { Component } from 'react'
import { View, Button, Text } from 'native-base'
import {connect} from "react-redux"
import { increment, reset } from "../store"
export class DemoCounter extends Component {
private timerID: number = 0
private interval = 1000
private startTimer(): void {
clearInterval(this.timerID)
this.timerID = setInterval(() => {
increment // Does nothing
}, this.interval)
}
componentDidMount(): void {
this.startTimer()
}
render() {
return (
<View>
<Button onPress={increment}> /* Does nothing */
<Text>Reset A</Text>
</Button>
<Button onPress={() => {reset(); this.startTimer()}}> /* How to combine action with other behaviors? */
<Text>Reset B</Text>
</Button>
<Button onPress={reset}> /* Does nothing */
<Text>Reset C</Text>
</Button>
</View>
)
}
}
const mapDispatchToProps = {
increment,
reset,
}
export default connect(null, mapDispatchToProps)(DemoCounter)
store.ts:
import {createStore} from "redux"
interface CounterState {
count: number;
}
const initialState: CounterState = {count: 0}
export type CounterAction =
| { type: 'INCREMENT' }
| { type: 'RESET' }
export const increment = (): CounterAction => ({ type: "INCREMENT" })
export const reset = (): CounterAction => ({ type: "RESET" })
const counterReducer = (state = initialState, action: CounterAction): CounterState => {
switch (action.type) {
case 'INCREMENT':
return {...state, count: state.count + 1}
case "RESET":
return {...state, count: 1}
default:
return state
}
}
export const store = createStore(counterReducer)
App.tsx:
import React, { Component } from 'react'
import { Provider } from 'react-redux'
import { store } from "./store"
import PerspectiveCounter from "./components/PerspectiveCounter"
export default class App extends Component {
render() {
return (
<Provider store={store}>
<DemoCounter />
</Provider>
)
}
}
Hmmm...
I have never done it like that, so I am not sure what the problem is...
How about trying to define it my way? ;)
At least as a temporary workaround.
const mapDispatchToProps = (dispatch) => {
return {
increment: () =>
dispatch({ type: '"INCREMENT"'})
}
}
Any calling this.props.increment, of course.
Below are the relevant files.
In the reducer, when it runs...
return {
loggedIn: action.loggedIn
};
I was expecting it to replace the state with that information.
When I run this code in LoginForm I get the old state output.
this.props.onLogin();
console.log(this.props.loggedIn);
I'm hoping I'm overlooking something simple here. Everything else seem to work the way I was expecting it to. I can change the state directly in the
switch using...
state.loggedIn = action.loggedIn;
And it works as expected. Can anyone shed some light on what I am doing wrong?
Action
import { LOGGED_IN } from './actionTypes';
export const loggedIn = () => {
return {
type: LOGGED_IN,
loggedIn: true,
};
};
Reducer
import {
LOGGED_IN
} from "../actions/actionTypes";
const initialState = {
loggedIn: false,
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case LOGGED_IN:
return {
loggedIn: action.loggedIn
};
default:
return state;
}
};
export default reducer;
import React, { Component } from 'react';
import { Text } from 'react-native';
import { Button, Card, CardSection, Input, Spinner } from './common';
import { Actions } from 'react-native-router-flux';
import firebase from '../Fire';
import { connect } from "react-redux";
import {
loggedIn
} from "../store/actions";
LoginForm
class LoginForm extends Component {
onButtonPress() {
this.onLoginSuccess();
}
onLoginSuccess() {
this.props.onLogin();
console.log(this.props.loggedIn);
Actions.main({});
}
renderButton() {
return (
<Button onPress={this.onButtonPress.bind(this)}>
Log in
</Button>
);
}
render() {
return (
<Card>
<CardSection>
<Input
placeholder="user#gmail.com"
label="Email"
</CardSection>
<CardSection>
<Input
secureTextEntry
placeholder="password"
label="Password"
/>
</CardSection>
<CardSection>
{this.renderButton()}
</CardSection>
</Card>
);
}
}
const styles = {
errorTextStyle: {
fontSize: 20,
alignSelf: 'center',
color: 'red'
}
};
const mapStateToProps = state => {
return {
loggedIn: state.loggedIn
};
};
const mapDispatchToProps = dispatch => {
return {
onLogin: () => dispatch(loggedIn()),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(LoginForm);
configureStore
import { createStore, combineReducers } from 'redux';
import prolinkReducer from './reducers/prolink';
const rootReducer = combineReducers({
loggedIn: prolinkReducer
});
const configureStore = () => {
return createStore(rootReducer);
};
export default configureStore;
The reason that you are getting the previous value is that you are console logging the previous value.
When the onLoginSuccess is called the current value for this.props.loggedIn will be passed to the console.log. I imagine if you set a long enough timeout on it then it would show that it is being updated, but that is not exactly the best way to check.
componentDidUpdate
If you want to check that your redux state is updating you should check what is happening in the componentDidUpdate https://reactjs.org/docs/react-component.html#componentdidupdate
As you subscribe to loggedIn in your mapStateToProps in your LoginForm.js, that means your component will receive the new value for loggedIn once it is updated.
In your LoginForm.js add the following:
componentDidUpdate(prevProps, prevState) {
console.warn('previous', prevProps.loggedIn, 'current', this.props.loggedIn)
}
This will allow you to see the values for loggedIn as it changes.
react-native-debugger
You could use react-native-debugger which includes redux inspection tools. https://github.com/jhen0409/react-native-debugger. This allows you to see your redux store in real-time, meaning you can easily track the changes without having to resort to checking in the componentDidUpdate. However, at this time there is currently an issue with react-native-debugger that means it is not working with react-native 0.58.+, though there is an open pull request that fixes the issue.
middleware
Alternatively you could add a middleware to your redux setup that logs each event to your console. https://redux.js.org/advanced/middleware, I have previously used redux-logger it is quite customisable, and depending on your use cases you may find it suits your needs.
In my React-Native app, I had some database code that worked fine. However, I decided that I needed to shoehorn in redux to maintain certain state, especially app settings.
Once I got the redux concepts through my thick skull and implemented it, that same database code started returning promises instead of honoring the "await" statements that were previously in use.
Here is the relevant reducer and database code:
// relevant imports
export default function divisionReducer(state = {programId: 1}, action) {
switch (action.type) {
case GET_DIVISIONS_BY_PROGRAM:
// add result to state
return _.cloneDeep({...state, divisions: divisions });
default:
return state;
}
}
getAllDivisions = async (programId) => {
let db = await openDefault();
const sql = "SELECT * FROM DIVISION WHERE DIVISION_PROGRAM_ID = ?";
let sqlResult = await query(db, sql, [programId]);
await close(db);
// function to convert db snake case to camelcase
result = keysToCamelCase(sqlResult.result);
return result;
}
My question: why is this code not honoring the "await" keywords?
Edit: More Code Added by Request
Below is the divisionAction code:
import { GET_DIVISIONS_BY_PROGRAM } from "./actionTypes";
export const getAllDivisions = (programId) => {
return {
type: GET_DIVISIONS_BY_PROGRAM,
payload: programId
}
}
Below is the DivisionManagementScreen, which calls the getAllDivisions code:
port React, {Component} from "react";
import {View, FlatList, Alert} from "react-native";
import {withNavigation} from "react-navigation";
import {connect} from "react-redux";
import masterStyles, {listPage, bigButtonStyles} from "./../../styles/master";
import {getAllDivisions} from "./../../redux/actions/divisionActions";
import DivisionManagementRow from "./DivisionManagementRow";
class DivisionManagmementScreen extends Component {
constructor(props) {
super(props);
}
async componentDidMount() {
this.props.getAllDivisions(this.props.programId);
console.log("Props after getAllDivisions: " + JSON.stringify(this.props));
}
async componentWillUnmount() {
console.log("Entered componentWillUnount()");
}
_renderItem = ({item}) => (
<DivisionManagementRow divisionId={item.DIVISION_ID} divisionName={item.DIVISION_NAME}
onAddTeam={() => {this._addTeam(item.DIVISION_ID)}}
onEdit={() => {this._editDivision(item.DIVISION_ID)}}
onDelete={() => {this._btnDeleteDivision(item.DIVISION_ID)}}/>
);
render() {
console.log("In render(), props: " + JSON.stringify(this.props));
return (
<View style={masterStyles.component}>
<View style={listPage.listArea}>
<FlatList
data={this.props.divisions}
renderItem={this._renderItem}
keyExtractor={(item) => item.DIVISION_ID.toString() } />
</View>
<View style={listPage.bottomButtonArea}>
<PortableButton defaultLabel="Add Division"
disabled={false}
onPress={() => {this._addDivision()}}
onLongPress={() => {}}
style={bigButtonStyles} />
</View>
</View>
);
}
}
function mapStateToProps(state) {
return {
programId: state.divisionReducer.programId,
divisions: state.divisionReducer.divisions
};
}
export default withNavigation(connect(mapStateToProps, {getAllDivisions})(DivisionManagmementScreen));
Is this enough code to diagnose?
I can't see where you're actually calling your getAllDivisions async function. I can only see you trying to call the getAllDivisions action creator - action creators just emit actions syncronously, by default they can't call functions with side effects.
If you want to trigger side effects, like your DB async function you need to look into a library like redux-thunk. Or more advanced would be redux-saga. If you're new to this stuff, I advise starting with redux-thunk.
Also I think the way you're using the connect() function is wrong. The second argument mapDispatchToProps needs to actually dispatch your actions to the store. So it should look like this:
function mapStateToProps(state) {
return {
programId: state.divisionReducer.programId,
divisions: state.divisionReducer.divisions
};
}
function mapDispatchToProps(dispatch) {
return {
getAllDivisions () {
dispatch(getAllDivisions())
}
};
}
export default withNavigation(
connect(
mapStateToProps, mapDispatchToProps
)(DivisionManagmementScreen)
)
So, instead of writing my database-enabled action creator correctly (starting with " return (dispatch) => { /* blah blah blah */ } ), I was still having it return an object, and having the reducer call the method with the database code.
I finally got the concepts through my thick skull, and got the code working over a weekend.
I need some help with my app and Redux! (Currently, i hate it aha)
So, i have a notification page component which fetch some datas and i need to put the data length into my redux store to put badge on my icon in my tabbar!
My Main Reducer :
import { combineReducers } from "redux";
import NotificationReducer from "./NotificationReducer";
export default function getRootReducer(navReducer) {
return combineReducers({
nav: navReducer,
notificationReducer: NotificationReducer
});
}
My Notification reducer
const initialState = {
NotificationCount: 0
};
export default function notifications(state = initialState, action = {}) {
switch (action.type) {
case 'SET_COUNT' :
console.log('REDUCER NOTIFICATION SET_COUNT',state)
return {
...state,
NotificationCount: action.payload
};
default:
return state;
}
};
My Action :
export function setNotificationCount(count) {
return function (dispatch, getState) {
console.log('Action - setNotificationCount: '+count)
dispatch( {
type: 'SET_COUNT',
payload: count,
});
};
};
My Component :
import React, { Component } from 'react';
import { View, Text, StyleSheet, ScrollView, Dimensions, TouchableOpacity, SectionList, Alert } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import { Notification } from '#Components';
import { ORANGE } from '#Theme/colors';
import { NotificationService } from '#Services';
import Style from './style';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as Actions from '#Redux/Actions';
const width = Dimensions.get('window').width
const height = Dimensions.get('window').height
export class NotificationsClass extends Component {
constructor(props) {
super(props);
this.state = {
dataSource: [],
NotificationCount: undefined
};
}
async componentWillMount() {
this.updateNotifications();
}
componentWillReceiveProps(nextProps){
console.log('receive new props',nextProps);
}
async updateNotifications() {
this.props.setNotificationCount(10); <---
let data = await NotificationService.get();
if (data && data.data.length > 0) {
this.setState({ dataSource: data });
console.log(this.props) <-- NotificationCount is undefined
}
}
render() {
if (this.state.dataSource.length > 0) {
return (
<SectionList
stickySectionHeadersEnabled
refreshing
keyExtractor={(item, index) => item.notificationId}
style={Style.container}
sections={this.state.dataSource}
renderItem={({ item }) => this.renderRow(item)}
renderSectionHeader={({ section }) => this.renderSection(section)}
/>
);
} else {
return this.renderEmpty();
}
}
renderRow(data) {
return (
<TouchableOpacity activeOpacity={0.8} key={data.notificationId}>
<Notification data={data} />
</TouchableOpacity>
);
}
}
const Notifications = connect(
state => ({
NotificationCount: state.NotificationCount
}),
dispatch => bindActionCreators(Actions, dispatch)
)(NotificationsClass);
export { Notifications };
(I've removed some useless code)
Top Level :
const navReducer = (state, action) => {
const newState = AppNavigator.router.getStateForAction(action, state);
return newState || state;
};
#connect(state => ({
nav: state.nav
}))
class AppWithNavigationState extends Component {
render() {
return (
<AppNavigator
navigation={addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.nav,
})}
/>
);
}
}
const store = getStore(navReducer);
export default function NCAP() {
return (
<Provider store={store}>
<AppWithNavigationState />
</Provider>
);
}
React : 15.6.1
React-Native : 0.46.4
Redux : 3.7.2
React-Redux : 5.0.5
React-Navigation : 1.0.0-beta.11
Node : 6.9.1
So if you've an idea! It will be great :D !
Thanks !
There's three issues.
First, React's re-rendering is almost always asynchronous. In updateNotifications(), you are calling this.props.setNotificationCount(10), but attempting to view/use the props later in that function. Even with the await in there, there's no guarantee that this.props.NotificationCount will have been updated yet.
Second, based on your reducer structure and mapState function, props.NotificationCount will actually never exist. In your getRootReducer() function, you have:
return combineReducers({
nav: navReducer,
notificationReducer: NotificationReducer
});
That means your root state will be state.nav and state.notificationReducer. But, in your mapState function, you have:
state => ({
NotificationCount: state.NotificationCount
}),
state.NotificationCount will never exist, because you didn't use that key name when you called combineReducers.
Third, your notificationReducer actually has a nested value. It's returning {NotificationCount : 0}.
So, the value you actually want is really at state.notificationReducer.NotificationCount. That means your mapState function should actually be:
state => ({
NotificationCount: state.notificationReducer.NotificationCount
}),
If your notificationReducer isn't actually going to store any other values, I'd suggest simplifying it so that it's just storing the number, not the number inside of an object. I'd also suggest removing the word Reducer from your state slice name. That way, you could reference state.notification instead.
For more info, see the Structuring Reducers - Using combineReducers section of the Redux docs, which goes into more detail on how using combineReducers defines your state shape.
In my react native app I'm using redux to handle state transition of a Post object -- the state is changed by couple of child components. The Post object has properties like title, name, description which the user can edit and Save.
In the reducer Im using React.addons.update return new state object.
The main container view has 2 custom child components (wrapped in TabBarNavigator).
One of the child component has few TextInputs which is updating a state.
Using the logger middleware and console.log() I see the new state value in the parent view's render() (via this.props.name) but not in the child view.
I'm trying to figure out why the updated state is not propagated to the child container. Any suggestion is much appreciated.
Im at a point where Im thinking of subscribeing to the redux store manually in the child container but it feels wrong
my code looks like this:
MainView
Reducer
configure store etc
The MainView
const React = require('react-native');
const {
Component,
} = React;
const styles = require('./../Styles');
const MenuView = require('./MenuView');
import Drawer from 'react-native-drawer';
import TabBarNavigator from 'react-native-tabbar-navigator';
import BackButton from '../components/BackButton';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as PostActions from '../actions/Actions';
import {Details} from './Article/Details';
import {ArticleSecondary} from './Article/Secondary';
var update = require('react-addons-update');
import configureStore from '../store/configureStore';
class ArticleMainView extends Component {
constructor(props){
super(props);
//var store = configureStore(props.route.post);
this.state = {
};
}
componentDidMount(){
}
savePost() {
console.log(this.props.post.data);
this.props.navigator.pop();
}
render(){
console.log("ArticleMainView: render(): " + this.props.name);
return(
<TabBarNavigator
ref="navComponent"
navTintColor='#346293'
navBarTintColor='#94c1e8'
tabTintColor='#101820'
tabBarTintColor='#4090db'
onChange={(index)=>console.log(`selected index ${index}`)}>
<TabBarNavigator.Item title='ARTICLE' defaultTab>
<Details ref="articleDetail"
backButtonEvent={ () => {
this.props.navigator.pop();
}}
saveButtonEvent={ () => {
this.savePost();
}}
{...this.props}
/>
</TabBarNavigator.Item>
<TabBarNavigator.Item title='Secondary'>
<ArticleSecondary ref="articleSecondary"
{...this.props}
backButtonEvent={ () => {
this.props.navigator.pop();
}}
saveButtonEvent={ () => {
this.savePost();
}}
/>
</TabBarNavigator.Item>
</TabBarNavigator>
);
}
}
function mapStateToProps(state) {
return {
post: state,
text: state.data.text,
name: state.data.name,
description: state.data.description
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(PostActions, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(ArticleMainView);
The Reducer:
import {Constants} from '../api/Constants';
var update = require('react-addons-update');
export default function postReducer(state, action) {
switch(action.type) {
case Constants.SET_POST_TEXT:
if( state.data.text){
return update(state, {
data: { $merge: {text: action.text }}
});
}else{
return update(state, {
data: { $merge: {text: action.text }}
});
}
break;
case Constants.SET_POST_NAME:
return update(state, {
data: { name: { $set: action.text }}
});
return newO;
break;
case Constants.SET_POST_DESCRIPTION:
return update(state, {
data: { description: { $set: action.text }}
});
break;
default:
return state;
}
}
render scene of the app:
renderScene(route, navigator) {
switch (route.id) {
case "ArticleMainView":
let store = configureStore(route.post);
delete route.post; // TODO: not sure if I should remove this
return (
<Provider store={store}>
<ArticleMainView navigator={navigator} {...route}/>
</Provider>
);
default:
return <LandingView navigator={navigator} route={route}/>
}
}
configureStore:
import { createStore,applyMiddleware,compose } from 'redux'
import postReducer from '../reducers/SocialPostReducer';
import createLogger from 'redux-logger';
const logger = createLogger();
export default function configureStore(initialState){
return createStore(
postReducer,
initialState,
compose(applyMiddleware(logger))
);
}
If anyone stumbles on this question this is how I solved it. In each of the child components I declared a contextTypes object like so
ChildComponentView.contextTypes = {
store: React.PropTypes.object
}
to access the current state in the child component
let {store} = this.context;
store.getState();
I don’t know React Native well but something that threw me off is that you’re effectively creating a store on every render:
case "ArticleMainView":
let store = configureStore(route.post);
delete route.post; // TODO: not sure if I should remove this
return (
<Provider store={store}>
<ArticleMainView navigator={navigator} {...route}/>
</Provider>
);
Store should only be created once per application lifetime. It never makes sense to create it inside render() or renderScene() or similar methods. Please check the official Redux examples to see how the store is typically created.
Another problem is that you don’t show how you update the data, which child component doesn’t get updated, when you expect it to get updated, and so on. This is a lot of code, and it is very hard to help because it is incomplete, and most of it is not relevant to the problem. I would suggest you to remove all the irrelevant code until you can reproduce the problem with a minimal possible complete example. Then you can amend your question to include that example.