Consider the following code. It's supposed to work like this; keystrokes are supposed to update the local state and once the button is clicked, the global state should be updated. And the component itself is responsible to show the global state as well.
class App extends React.Component {
constructor(props) {
super(props);
this.state = { name: this.props.appState.name, localName: "" };
this.nameChanged = this.nameChanged.bind(this);
this.sendAction = this.sendAction.bind(this);
}
nameChanged(event) {
this.setState(Object.assign({}, this.state, { localName: event.target.value }));
}
sendAction(event) {
this.props.saveName(this.state.localName);
}
render() {
return (
<div>
<h1>Hello, {this.state.name}!</h1>
<input type="text" value={this.state.localName} onChange={this.nameChanged} />
<input type="button" value="Click me!" onClick={this.sendAction} />
</div>
);
}
}
const AppContainer = ReactRedux.connect(
state => ({ appState: state.appReducer.appState }),
dispatch => Redux.bindActionCreators({
saveName: (name) => ({ type: "SAVE_NAME", name })
}, dispatch)
)(App);
const appReducer = (state = { appState: { name: "World!" } }, action) => {
switch (action.type) {
case "SAVE_NAME":
return Object.assign({}, state, { name: action.name });
default:
return state;
}
};
const combinedReducers = Redux.combineReducers({
appReducer
});
const store = Redux.createStore(combinedReducers);
ReactDOM.render(
<ReactRedux.Provider store={store}>
<AppContainer />
</ReactRedux.Provider>,
document.getElementsByTagName('main')[0]
);
Right now, the local state is updated correctly. But when I click the button, even though the action is created and sent, but I don't see the global state with the new value in the UI. I'm not sure if the global state is not updated or the component is not properly informed.
Change <h1>Hello, {this.state.name}!</h1> to this:
<h1>Hello, {this.props.appState.name}!</h1>
You don't have to set the state locally, Redux is updating the props for you through connect when the store state changes. The other change you'll need to make is in your reducer. Your current state is pretty deeply nested and it's not being returned how you're expecting it to when you dispatch your action. Here's the updated version:
case "SAVE_NAME":
return {
...state,
appState: {
...state.appState,
name: action.name
}
}
Here's a working version in CodePen Link
Related
When I add a new product to the products array using the addQuantityButton function via the child component, the change to the products array isn't recognised in the child Product component. addQuantityButtonfunction in the parent component is correctly adding a new object to the array. This stopped working when I started using Redux and mapStateToProps. What am I doing wrong here?
Path: Parent
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
products: [getNewProduct()]
};
this.addQuantityButton = this.addQuantityButton.bind(this);
}
addQuantityButton(product_id) {
let { products } = this.state;
products = products.map((product) => {
if (product.product_id === product_id) {
product.quantities.push(getNewQuantity());
return product;
}
return product;
});
this.setState({
products
});
}
render() {
const { products } = this.state;
return (
<form>
{products.map((product) => (
<Product
key={product.key}
product_id={product.product_id}
quantities={product.quantities}
addQuantityButton={this.addQuantityButton}
/>
))}
</form>
);
}
}
Path: Product
class Product extends React.Component {
constructor(props) {
super(props);
this.state = {
unitOptions: []
};
}
render() {
return (
<div>
<div>
{this.props.quantities.map((quantity) => (
<p>Example</p>
))}
</div>
<button
type="button"
onClick={() => addQuantityButton(this.props.product_id)}
>
Add quanity
</button>
</div>
);
}
}
Product.propTypes = {
product_id: PropTypes.string,
quantities: PropTypes.array,
addQuantityButton: PropTypes.func
};
const mapStateToProps = (state) => ({
supplierProducts: state.product.products
});
export default connect(mapStateToProps)(Product);
child is listening to redux's state, which is different from parent's state. react component state is one thing, redux state is other. copying redux's state to a component's state is not advisable, you are duplicating state.
I would suggest at Parent's to map only your products to props, then iterate at your form as this.props.products.map(...
while at Children you declare a mapDispatchToProps responsible to increment the quantity. there you declare your addQuantityButton with some refactors. you will use dispatch instead which receives an action. the logic to add product will be implemented at your reducer down the road.
const mapDispatchToProps = (dispatch, ownProps) => ({
addQuantityButton: dispatch(addQuantity(ownProps.product_id))
})
your action is a simple function declared at some actions file, that return an object containing the action TYPE and a payload (you could call the payload something else fwiw):
const addQuantity = product_id => ({
type: 'ADD_QUANTITY',
payload: product_id
})
now, dispatch with a proper action will pass down the object to reducers, and a given reducer that intercepts ADD_QUANTITY will be responsible to increment quantity, and that way return next redux state.
at reducer you implement the logic to update state.
function productsReducer(state = initialState, action) {
switch (action.type) {
case 'ADD_QUANTITY': // suggestion, declare your types as a constant in another file
// also, dont mutate state directly!, you may need to use some deep clone given it's an array of objects
return // logic here with action.payload and state.products
default:
return state
}
}
I'm learning redux by using it with a react app that pulls articles from the Hacker News API.
I want to save the search terms inputted in the search bar to the redux state, but when I look at the state, the searchTerm state I have made is empty in redux devtools.
Here is my search bar component:
import React, { Component } from 'react'
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import getSearchTerm from '../actions/searchAction';
class Search extends Component {
constructor(props) {
super(props);
this.state = {
searchTerm: ''
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
onChange(e) {
this.setState({ [e.target.name]: e.target.value });
}
onSubmit(e) {
e.preventDefault()
const searchTerm = this.state.searchTerm
this.props.getSearchTerm(searchTerm);
}
render() {
return (
<div>
<form onSubmit={this.onSubmit}>
<input
type="text"
name="searchTerm"
style={{
width: "95%",
paddingTop: 8,
paddingBottom: 8,
paddingLeft: 16,
fontSize: 24
}}
placeholder="Enter term here" onChange={this.onChange}
value={this.state.searchTerm} />
<button type="submit">Submit</button>
</form>
</div>
)
}
}
Search.propTypes = {
searchTerm: PropTypes.string.isRequired
}
const mapPropsToState = state => ({
getSearchTerm: state.searchTerm
})
export default connect(mapPropsToState, { getSearchTerm })(Search)
Here is the action I am using to save the search term:
export const getSearchTerm = term => dispatch => {
console.log('action called')
dispatch({
type: GET_TERM,
term
})
}
And here is the reducer:
import GET_TERM from '../actions/types';
const initState = {
searchTerm: null
}
export default function (state = initState, action = {}) {
switch (action.type) {
case GET_TERM:
return {
searchTerm: action.term
}
default:
return state
}
}
I am also getting a warning in my console saying:
'Failed prop type: The prop searchTerm is marked as required in
Search, but its value is undefined.'
Obviously, this means the search term I have required is undefined because I have it as undefined in the reducer. However, I want to save the search term I enter into the redux state. How do I go about this?
Can your check if your reducer is getting it. Just console.log() it before returning new state in your reducer.
Also use should always spread the state in every return in order to ensure that your overall state doesn't get overwritten.
Posted as I don't have comment privileges.
I am trying to display my state (users) in my react/redux functional component:
const Dumb = ({ users }) => {
console.log('users', users)
return (
<div>
<ul>
{users.map(user => <li>user</li>)}
</ul>
</div>
)
}
const data = state => ({
users: state
})
connect(data, null)(Dumb)
Dumb is used in a container component. The users.map statement has an issue but I thought that the data was injected through the connect statement? the reducer has an initial state with 1 name in it:
const users = (state = ['Jack'], action) => {
switch (action.type) {
case 'RECEIVED_DATA':
return action.data
default:
return state
}
}
CodeSandbox
You aren't using the connected component while rendering and hence the props aren't available in the component
const ConnectedDumb = connect(
data,
null
)(Dumb);
class Container extends React.Component {
render() {
return (
<div>
<ConnectedDumb />
</div>
);
}
}
Working demo
I' trying to make a real time application with react, redux and redux-thunk, that gets the objects from back-end through socket with STOMP over sockJS, and update redux store every time an object comes and finally updates the container when redux store updates.
My connect class through stomp over sockjs is this;
class SearcButtons extends Component {
render() {
return (
<div className="searchbuttons">
<RaisedButton className="bttn" label="Start" onClick={() => this.start_twitter_stream()} />
<RaisedButton className="bttn" label="Start" onClick={() => this.stop_twitter_stream()} />
</div>
);
}
start_twitter_stream() {
let stompClient = null;
var that = this;
let socket = new SockJS('http://localhost:3001/twitterStream');
stompClient = Stomp.over(socket);
stompClient.debug = null;
stompClient.connect({}, function () {
stompClient.subscribe('/topic/fetchTwitterStream', function (tokenizedTweet) {
let tweet = JSON.parse(tokenizedTweet.body);
let payload = {
data: {
tweets: that.props.state.reducer.tweets,
}
}
payload.data.tweets.push(
{
"username": tweet.username,
"tweet": tweet.tweet,
}
);
that.props.actions.update_tweets_data(payload);
});
stompClient.send("/app/manageTwitterStream", {}, JSON.stringify({ 'command': 'start', 'message': that.props.state.reducer.keyword }));
let payload = {
data: {
socketConnection: stompClient
}
}
that.props.actions.start_twitter_stream(payload);
});
}
stop_twitter_stream() {
var socketConnection = this.props.state.reducer.socketConnection;
socketConnection.send("/app/manageTwitterStream", {}, JSON.stringify({ 'command': 'stop', 'message': null }));
socketConnection.disconnect();
let payload = {
data: {
socketConnection: null
}
}
return this.props.actions.stop_twitter_stream(payload);
}
}
SearcButtons.propTypes = {
actions: PropTypes.object,
initialState: PropTypes.object
};
function mapStateToProps(state) {
return { state: state };
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(SearcButtons);
I'm calling tweet panel container inside App.js
import TweetPanel from './containers/TweetPanel';
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
class App extends Component {
render() {
return (
<MuiThemeProvider>
<div className="main">
<TweetPanel />
</div>
</MuiThemeProvider>
);
}
}
export default App;
My container that listens redux-store is this;
class TweetPanel extends Component {
const TABLE_COLUMNS = [
{
key: 'username',
label: 'Username',
}, {
key: 'tweet',
label: 'Tweet',
},
];
render() {
console.log(this.props);
return (
<DataTables
height={'auto'}
selectable={false}
showRowHover={true}
columns={TABLE_COLUMNS}
data={
(typeof (this.props.state.reducer.tweets) !== "undefined" ) ?this.props.state.reducer.tweets : []
}
showCheckboxes={false}
onCellClick={this.handleCellClick}
onCellDoubleClick={this.handleCellDoubleClick}
onFilterValueChange={this.handleFilterValueChange}
onSortOrderChange={this.handleSortOrderChange}
page={1}
count={100}
/>
);
}
}
TweetPanel.propTypes = {
actions: PropTypes.object,
initialState: PropTypes.object
};
function mapStateToProps(state) {
return { state: state };
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(actions, dispatch)
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(TweetPanel);
My actions;
import {
BUILD_TWITTER_STREAM,
START_TWITTER_STREAM,
UPDATE_TWEETS_DATA,
} from '../actions/action_types';
export function build_twitter_stream(state) {
return {
type: BUILD_TWITTER_STREAM,
payload: state
};
}
export function start_twitter_stream(state) {
return {
type: START_TWITTER_STREAM,
payload: state
};
}
export function update_tweets_data(state) {
return {
type: UPDATE_TWEETS_DATA,
payload: state
};
}
My reducer;
import update from 'immutability-helper';
let initialState = {
socketConnection : null,
tweets : [ ]
}
export default function reducer(state = initialState, action) {
switch (action.type) {
case BUILD_TWITTER_STREAM:
return update(
state, {
socketConnection: { $set: action.payload.data.socketConnection }
}
);
case START_TWITTER_STREAM:
return update(
state, {
socketConnection: { $set: action.payload.data.socketConnection }
}
);
case UPDATE_TWEETS_DATA:
return update(
state, {
tweets: { $merge: action.payload.data.tweets }
}
);
default:
return state;
}
}
My observations are when I try to connect to socket through stomp over Sockjs, I need to pass the context as named "that" variable which you can see the first code block above and update redux store with that context in stompClient's connect function's callback, which means I update store in an asynchronou function, redux store updates very well when I look to Chrome' s extension of Redux devtools, but container doesn't update unless I press to the stop button which triggers an action which is not asynchronous.
Thanks in advance, your help is much appreciated :)
I can offer another approach, function delegate approach, since I have struggled by similar issiues. I create props in components, like your RaisedButton. For example I create BindStore props, such as:
<RaisedButton BindStore={(thatContext)=>{thatContext.state = AppStore.getState();}}... />
Also I can add subscriber props, such as:
<RaisedButton SubscribeStore={(thatContext) => {AppStore.subscribe(()=>{thatContext.setState(AppStore.getState())})}} ... />
At the RaisedButton.js, I can give thatContext easily:
...
constructor(props){
super(props);
if(this.props.BindStore){
this.props.BindStore(this);
}
if(this.props.SubscribeStore){
this.props.SubscribeStore(this);
}
}
...
Also by doing so, means by using props, one RaisedButton may not have BindingStore ability or SubscribeStore ability. Also by props I can call store dispatchers, such as:
in parent.js:
<RaisedButton PropsClick={(thatContext, thatValue) => {AppStore.dispacth(()=>
{type:"ActionType", payload:{...}})}} ... />
in RaisedButton.js
//as an example I used here dropDown, which is:
import { Dropdown } from 'react-native-material-dropdown';
//react-native-material-dropdown package has issues but the solutions are in internet :)
...
render(){
return(
<View>
<Dropdown onChangeText={(value) => {this.props.PropsClick(this, value);}} />
</View>
)
}
...
In many examples, for instance your parent is SearchButtons, the parent must be rendered, the parent must subscribe the store so when any child changes the store all component cluster is rerendered. But, by this approach, children components are subscribed and bound to the store. Even one child may dispatch an action and after that, other same type children subscribed function is called back and only the subscribed children is rerendered. Also, you will connect only one component to the redux store, the parent.
//parent.js
import { connect } from 'react-redux';
import AppStore from '../CustomReducer';
...
//you will not need to set your parent's state to store state.
I did not investigate very much about this approach but I do not use mapping functions. However, the child components will access all store datas, but also in mappings all store datas are also accessible.
I have been using react+redux quite while, but could you any one help me the following case, on codepen:
const {createStore } = Redux;
const { Provider, connect } = ReactRedux;
const store = createStore((state={name: 'ron'}, action) => {
switch(action.type) {
case 'changeName': return {name: action.name};
default: return state
}
})
const Person = props => {
const {name, dispatch} = props
console.log(`rendering Person due to name changed to ${name}`)
return (
<div>
<p> My name is {name} </p>
<button onClick={ () => dispatch({type: 'changeName', name: 'ron'}) } > Change to Ron </button>
<button onClick={ () => dispatch({type: 'changeName', name: 'john'}) } > Change to John</button>
</div>
)
}
const App = connect(state=>state)(Person)
ReactDOM.render(
<Provider store={store}><App/></Provider>,
document.getElementById('root')
);
It is simple react app, but I cannot explain:
Initialise redux store with one reducer, and its initValue is {name: 'ron'}
Click Change to ron button, it will dispatch {type: 'changeName', name: 'ron'}
When the reducer get this action, it will generate an brand new state {name: 'ron'}, though the value is same as the original state, but they are different identity and should be the different ones.
The functional component should be re-rendered if the props changed even though the values are the same. So I suppose the render function will be called, and console should output rendering Person due to.... However, it is not happening.
I am wondering why react functional component refuse to render again when the props identity are changed (though the values are the same)
Your connect(state=>state)(Person) I think it's not wrong but it's weird.
According to the documentation https://redux.js.org/docs/basics/UsageWithReact.html you can separate the state and the action dispatcher, commonly naming mapStateToProps and mapDispatchToProps.
So, I propose to you this code:
const mapStateToProps = state => ({
user: state.user
})
const mapDispatchToProps = dispatch => ({
updateName: (name) => dispatch(changeName(name)),
})
class DemoContainer extends Component {
constructor() {
super();
}
render() {
return (
<div>
<p> My name is {this.props.user.name}</p>
<button onClick={ () => this.props.updateName('ron') } > Change to Ron </button>
<button onClick={ () => this.props.updateName('john') } > Change to John</button>
</div>
);
}
}
const Demo = connect(
mapStateToProps,
mapDispatchToProps
)(DemoContainer)
export default Demo
My reducer:
const initialState = { name: 'John'}
const user = (state = initialState, action) => {
switch (action.type) {
case "CHANGE_NAME":
return {
name: action.name
}
default:
return state
}
}
export default user
My action:
export const changeName = ( name ) => ({
type: "CHANGE_NAME",
name,
})
You can check all my code here: https://stackblitz.com/edit/react-tchqrg
I have a class for the component but you can also use a functional component with connect like you do.