I'm creating some kind of Realtime Chat App with React Native + Redux. When I get some new message from websocket, internally it will updates list of message as array with redux store. This is what looks like:
import { CHAT_INIT, CHAT_RECV } from '../actions/Chat';
const defaultState = {
chatList: []
};
export default function(state = defaultState, action = {}) {
switch(action.type) {
case CHAT_INIT:
return Object.assign({}, {
chatList: []
});
case CHAT_RECV:
let chatList = state.chatList;
chatList.push(action.data);
return Object.assign({}, {
chatList: chatList
});
default:
return state;
}
}
There are only two actions: CHAT_INIT and CHAT_RECV which can easily understand.
When app receives new message from socket, it will invoke store.dispatch with 'CHAT_RECV' action. This is the component code of list of messages:
class ChatList extends Component {
static propTypes = {
chatList: React.PropTypes.array
}
static defaultProps = {
chatList: []
}
componentWillMount() {
store.dispatch({
type: ChatActions.CHAT_INIT,
data: ''
});
}
componentWillReceiveProps(nextProps) {
console.log('will receive props'); // 1
}
render() {
console.log('<ChatList />::chatList', this.props.chatList); // 2
return (
<View style={styles.chatList}>
<Text>ChatList</Text>
</View>
);
}
}
export default connect(state => {
let chatList = state.ChatReducer.chatList;
console.log('Got:', chatList); // 3
return {
chatList: state.ChatReducer.chatList
};
})(ChatList);
I connected ChatList component with ChatReducer.chatList so when new message arrives, props of ChatList component will be update.
The problem is props on ChatList component doesn't updating at all! As you can see, I placed lots of console.log to tracking where is the problem. Numbers next of console.log is just added for easy explanation.
You can see that I'm trying to update chatList props of connected component ChatList, and it should be re-render on receive new props(means new message).
So [3] of console.log prints 'Got: [..., ...]' as well, but [1] and [2] are not prints anything! It means ChatList component didn't receive next props properly.
I double checked the code and tried to fix this, but not much works. Is this problem of Redux or React-Redux module? Previously I used both modules for my Electron ChatApp, and it worked without any problem.
Is there a something that I missed? I really don't know what is the matter . Anyone knows about this issue, please gimme a hand, and will be very appreciate it.
P.S. These are other component codes. I think it doesn't important, but I just paste it for someone who wants to know.
Superior component: App.js
export default class App extends Component {
componentDidMount() {
init(); // this invokes CHAT_INIT action.
}
render() {
return (
<Provider store={store}>
<ChatApp />
</Provider>
);
}
}
ChatApp.js which actually renders ChatList component:
class ChatApp extends Component {
render() {
return (
<View style={styles.container}>
<NavBar username={this.props.username} connected={this.props.connected} />
<ChatList connected={this.props.connected} />
<ChatForm connected={this.props.connected} />
</View>
);
}
}
export default connect(state => {
return {
username: state.UserReducer.username,
connected: state.NetworkReducer.connected
};
})(ChatApp);
You're mutating your state here:
case CHAT_RECV:
let chatList = state.chatList;
chatList.push(action.data);
return Object.assign({}, {
chatList: chatList
});
Instead, do:
case CHAT_RECV:
let chatList = state.chatList.concat(action.data);
return Object.assign({}, {
chatList: chatList
});
Related
Trying to use a AsyncStorage variable to conditionally render content.
My app uses createBottomTabNavigator from react-navigation. I have a tab called Settings that must conditionally render content based on wether a user is logged in or not (checking AsyncStorage). The following code works on first render but another tab can update AsyncStorage value, returning back to Settings tab it still renders initial content.
Which approach can i use to achieve this, i'm also trying to use shouldComponentUpdate but i'm not sure how it works.
import React, {Component} from 'react';
class Settings extends React.Component{
constructor(props){
super(props);
this.state = {
isLoggedIn:false
};
}
//I want to use this method but not sure how.
shouldComponentUpdate(nextProps, nextState){
// return this.state.isLoggedIn != nextState;
}
componentDidMount(){
console.log("componentWillUpdate..");
this.getLocalStorage();
}
getLocalStorage = async () => {
try {
const value = await AsyncStorage.getItem('username');
if(value !== null) {
this.setState({isLoggedIn:true});
}
} catch(e) {
// error reading value
}
}
render() {
if(this.state.isLoggedIn)
{
return(
<View>
<Text style={styles.title_header}>Logged In</Text>
</View>
);
}
else{
return(
<View>
<Text style={styles.title_header}>Logged Out</Text>
</View>
);
}
}
}
export default Settings;
})
Use NavigationEvents. Add event listeners to your Settings components.
onWillFocus - event listener
onDidFocus - event listener
onWillBlur - event listener
onDidBlur - event listener
for example, the following will get fired when the next screen is focused.
focusSubscription = null;
onWillFocus = payload => {
// get values from storage here
};
componentDidMount = () => {
this.focusSubscription = this.props.navigation.addListener(
'willFocus',
this.onWillFocus
);
};
componentWillUnmount = () => {
this.focusSubscription && this.focusSubscription.remove();
this.focusSubscription = null;
};
The problem comes from react-navigation createBottomTabNavigator. On first visit, the component is mounted and so componentDidMount is called and everything is great.
However, when you switch tab, the component is not unmounted, which means that when you come back to the tab there won't be any new call to componentDidMount.
What you should do is add a listener to the willFocus event to know when the user switches back to the tab.
componentDidMount() {
this.listener = this.props.navigation.addListener('willFocus', () => {
AsyncStorage.getItem('username').then((value) => {
if (value !== null) {
this.setState({ isLoggedIn: true });
}
catch(e) {
// error reading value
}
});
});
}
Don't forget to remove the listener when the component is unmounted:
componentWillUnmount() {
this.listener.remove();
}
I'm setting up an application in React-native where I have a:
Component A : a search component with 2 fields
Component B : a button on this page where I click on it, the 3rd field appears
This components are only linked with react-navigation
In my case, the component B is a component where I can buy premium, and I want to update the component A when premium bought.
The problem : when I already rendered the Component A, and the I go to Component B, click the button, the Component A does not re-render, how can I do it ?
I'm looking for something like this :
class ComponentA extends PureComponent {
render() {
if (userHasNotClickedOnComponentB) {
return (
<SearchForm/>
)
} else {
return (
<SearchFormPremium/>
)
}
}
}
SearchForm and SearchFormPremium are two separated Component:
One with the Premium functionalities, the other one for normal users only
I already rendered ComponentA, and then I go to ComponentB and click the button
class ComponentB extends PureComponent {
render() {
return (
<Button onClick={() => setPremium()}/>
)
}
}
How can the ComponentA re-render so i can have the changes of ComponentB ?
Thanks
You may want to look into using Redux, or something of the like to keep a centralized store that all of your components can look at. There are plenty of Redux tutorials out there so I wont go into details, but essentially it will allow you to:
1) Create a data store accessible from any 'connected' component
2) Dispatch actions from any component to update the store
When you connect a component, the connected data becomes props. So, for example, if you connected component A and B to the same slice of your store, when component A updates it, component B will automatically re-render because its props have changed.
Redux github page
Okay, with Redux it worked !
Just connect both component. In ComponentA (the component that has to be automatically updated) use the function componentWillReceiveProps() and refresh it inside of it.
In Reducer :
const initialState = {premium: false};
const tooglePremiumReducer = (state = initialState, action) => {
switch (action.type) {
case "TOOGLE_PREMIUM":
return {
...state,
premium: action.payload.premium,
};
default:
return state;
}
};
export default tooglePremiumReducer;
In Action :
export const tooglePremiumAction = (premium) => {
return dispatch => {
dispatch({
type: "TOOGLE_PREMIUM",
payload: {
premium: premium
}
});
};
};
In ComponentB :
// Import tooglePremiumAction
class ComponentB extends PureComponent {
render() {
return (
<Button onClick={() => this.props.tooglePremiumAction(true)}/>
)
}
}
const actions = {
tooglePremiumAction
};
export default connect(
actions
)(ComponentB);
In ComponentA:
class ComponentA extends PureComponent {
componentWillReceiveProps(nextProps) {
if(this.props.premium !== nextProps.premium) {
//here refresh your component
}
}
render() {
if (!this.props.premium) {
return (
<SearchForm/>
)
} else {
return (
<SearchFormPremium/>
)
}
}
}
const mapStateToProps = state => {
const premium = state.premium.premium
return { premium };
};
export default connect(mapStateToProps)(ComponentA);
In my code below you can see my component. How it is written will cause the app to crash with the error:
undefined is not an object (evaluation this.props.data.ID)
So in my componentDidMount that id variable is not receiving the props data.
However if i comment out that code in the componentDidMount the app will load fine and the props.data.ID will print out in View. Is there a reason why i can't access the props.data.ID in my componentDidMount?
Heres my code
// timeline.js
class TimelineScreen extends Component {
constructor(props) {
super(props);
this.state = {
posts: []
}
}
componentDidMount() {
const { id } = this.props.data.ID;
axios.post('/api/hometimeline', { id })
.then(res => {
this.setState({
posts: res.data
});
});
}
render() {
const { data } = this.props;
return (
<View style={s.container}>
{
data
?
<Text>{data.ID}</Text>
:
null
}
</View>
);
}
}
function mapStateToProps(state) {
const { data } = state.user;
return {
data
}
}
const connectedTimelineScreen = connect(mapStateToProps)(TimelineScreen);
export default connectedTimelineScreen;
The input of mapStateToProps is not react state, it is redux store. You shouldn't use this.setState in componentDidMount. Use redux actions and reducers to change redux store. Whenever redux store changes, it will invoke mapStateToProps and update your props
componentDidMount() {
console.log(this.props.data); // for test
const id = this.props.data.ID;
//OR
const {id} = this.props.data;
...
}
I have a container in my React Native app and and I use it like preload to show scene Loading... before I get data from server. So I dispatch an action to fetch user data and after that I update my state I try to push new component to Navigator but I've got an error:
Warning: setState(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount`.
And I don't understand what is the best way to fix my problem.
So my container:
import myComponent from '../components'
class App extends Component {
componentDidMount() {
this.props.dispatch(fetchUser());
}
_navigate(component, type = 'Normal') {
this.props.navigator.push({
component,
type
})
}
render() {
if (!this.props.isFetching) {
this._navigate(myComponent);
}
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Loading...
</Text>
</View>
);
}
}
App.propTypes = {
dispatch: React.PropTypes.func,
isFetching: React.PropTypes.bool,
user: React.PropTypes.string
};
export default connect((state) => ({
isFetching: state.data.isFetching,
data: state.data.user
}))(App);
My reducer:
const data = (state = initialState, action) => {
switch (action.type) {
case types.USER_FETCH_SUCCEEDED:
return {
...state,
isFetching: false,
user: action.user
};
default:
return state;
}
};
Don't trigger anything that can setState inside the body of your render method. If you need to listen to incoming props, use componentWillReceiveProps
Remove this from render():
if (!this.props.isFetching) {
this._navigate(myComponent);
}
and add componentWillReceiveProps(nextProps)
componentWillReceiveProps(nextProps) {
if (!nextProps.isFetching) {
this._navigate(myComponent);
}
}
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.