Got working App at: https://github.com/BeerDRinker/recompose-ref
Following code(commented part in /src/App.js) works as expected:
class App extends Component {
constructor(props) {
super(props);
this.node = React.createRef();
this.state = {
value: 1
};
}
handleTouchStart = e => {
e.preventDefault();
this.setState({ value: this.state.value + 1 });
};
handleTouchEnd = e => {
e.preventDefault();
this.setState({ value: this.state.value - 1 });
};
componentDidMount() {
this.node.current.ontouchstart = this.handleTouchStart;
this.node.current.ontouchend = this.handleTouchEnd;
}
render() {
return (
<div>
<h3>Value: {this.state.value}</h3>
<button ref={this.node}>Submit</button>
</div>
);
}
}
export default App;
But I need the same functionality by using Recompose. I tried, but got nothing working. My code sample(not commented part in /src/App.js) that don't works:
import React from "react";
import {
compose,
lifecycle,
setDisplayName,
withProps,
withStateHandlers
} from "recompose";
import "./App.css";
const state = {
value: 1
};
const stateHandlers = {
handleTouchStart: value => () => ({
value: value + 1
}),
handleTouchEnd: value => () => ({
value: value - 1
})
};
export const enhance = compose(
setDisplayName("App"),
withProps(props => ({
bookNode: React.createRef()
})),
withStateHandlers(state, stateHandlers),
lifecycle({
componentDidMount() {
this.bookNode.current.ontouchstart =
this.handleTouchStart;
this.bookNode.current.ontouchend = this.handleTouchEnd;
}
})
);
export const App = ({ value, bookNode }) => (
<div>
<h3>Value: {value}</h3>
<button ref={bookNode}>Submit</button>
</div>
);
export default enhance(App);
Just start using recompose, lot of things still magic for me ))
I hope some on can help me, pass several days to solve this problem.
There are problems in composed component.
There's no bookNode and event handlers on this. App is stateless component that doesn't have access to this, bookNode and event handlers are props.
It isn't value that is passed to state handlers, it's state, as the name suggests.
It should be:
const stateHandlers = {
handleTouchStart: state => () => ({
value: state.value + 1
}),
handleTouchEnd: state => () => ({
value: state.value - 1
})
};
export const enhance = compose(
setDisplayName("App"),
withProps(props => ({
bookNode: React.createRef()
})),
withStateHandlers(state, stateHandlers),
lifecycle({
componentDidMount() {
this.props.bookNode.current.ontouchstart = this.props.handleTouchStart;
this.props.bookNode.current.ontouchend = this.props.handleTouchEnd;
}
})
);
export const App = ({ value, bookNode }) => (
<div>
<h3>Value: {value}</h3>
<button ref={bookNode}>Submit</button>
</div>
);
Here's a demo.
Usually there's no reason to access DOM manually to set up events because React handles this. This eliminates the need for a ref and lifecycle hooks:
export const enhance = compose(
setDisplayName("App"),
withStateHandlers(state, stateHandlers)
);
const App = ({ value, handleTouchStart, handleTouchEnd }) => (
<div>
<h3>Value: {value}</h3>
<button onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd}>Submit</button>
</div>
);
Related
Consider the following example:
const useCounter = () => {
const [count, setCount] = useState(0);
return [ count, setCount ];
};
const Shower = () => {
const [ value ] = useCounter();
console.log(value); //stays 0
return value;
}
const Setter = () => {
const [ value, setValue ] = useCounter();
console.log(value); //updates on click
return <button onClick={() => setValue(value+1)}>
Add
</button>
}
const App = () => {
return (
<div className="App">
<Setter />
<Shower />
</div>
);
}
What am I doing wrong? I'd expect that it will use the same state no matter where and how many times it gets used, and if that state updates, it should update every component which uses it I think.
Any suggestions?
That's what react context api try to solve.
const CounterContext = React.createContext({
count: 0,
setCount: () => null
})
const CounterProvider = ({ children }) => {
const [count, setCount] = useState(0);
return (
<CounterContext.Provider value={{
count, setCount
}}>
{children}
</CounterContext.Provider>
)
}
const useCounter = () => {
return React.useContext(CounterContext)
};
useCounter will now provide you the same count and setCount in every component you call it.
To use it:
const Shower = () => {
const { count } = useCounter();
return count;
}
const Setter = () => {
const { count, setCount } = useCounter();
return <button onClick={() => setCount(count+1)}>
Add
</button>
}
const App = () => {
return (
<CounterProvider>
<div className="App">
<Setter />
<Shower />
</div>
</CounterProvider>
);
}
useState returns a pair of value and setter. A piece of data and a way to change it, but everytime you instantiate a new Component a new instace of this pair will be created as well. hooks are a great way to share statetul logic between components, not state itself. Shower get's called and a instance of useCounter is created. Setter gets called and a new instance is created. The structure is the same, the state is not.
To share state between components use props, redux or Context API
When sharing things between functional components, I like to use the pattern below, it is the redux-ish reusable version of Federkun's answer above:
// this component should be an ancestor of component sharing state
// note that it works no matter how deep your subcomponents are in the render tree
class SharedStateContextProvider extends React.Component {
/* static propTypes = {
sharedContext: PropTypes.object,
reducer: PropTypes.func,
initialState: PropTypes.object,
children: PropTypes.oneOfType([
PropTypes.node,
PropTypes.arrayOf(PropTypes.node),
]),
} */
constructor(props) {
super(props);
this.state = {
contextValue: { state: props.initialState, dispatch: this.handleDispatch },
};
}
handleDispatch = (action) => {
const { reducer } = this.props;
const { contextValue: { state: sharedState } } = this.state;
const newState = reducer(sharedState, action);
if (newState !== sharedState) {
this.setState(
() => ({
contextValue: { state: newState, dispatch: this.handleDispatch }
})
);
}
}
render() {
const { sharedContext: Context, children } = this.props;
const { contextValue } = this.state;
return (
<Context.Provider value={contextValue}>
{children}
</Context.Provider>
);
}
}
// the actual shared context
const CounterContext = React.createContext();
// add as much logic as you want here to change the state
// as you would do with redux
function counterReducer(state, action) {
switch(action.type) {
case 'setValue':
return {
...state,
value: action.data
};
default:
return state;
}
}
// counterContext is a prop so the dependency in injected
const Shower = ({ counterContext }) => {
// well known redux-ish interface
const { state, dispatch } = React.useContext(counterContext);
console.log(state.value);
return state.value;
}
// counterContext is a prop so the dependency in injected
const Setter = ({ counterContext }) => {
// well known redux-ish interface
const { state, dispatch } = React.useContext(counterContext);
console.log(state.value); //updates on click
return <button onClick={() => dispatch({ type: 'setValue', data: state.value+1 })}>
Add
</button>
}
// the actual shared state
const initialCounterState = { value: 0 };
const App = () => {
return (
<div className="App">
<SharedStateContextProvider
sharedContext={CounterContext}
reducer={counterReducer}
initialState={initialCounterState}
>
<Setter counterContext={CounterContext} />
<Shower counterContext={CounterContext} />
</SharedStateContextProvider>
</div>
);
}
const root = document.getElementById('root');
ReactDOM.render(<App />, root);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
I'm writing a independent modal using React and Redux. I pass from my environment variable if modal is visible and initial position and the rest of the state in redux store.
I've tried using react lifecycle methods to force update my app but nothing seems to work.
This is how I connect my App with store:
render() {
const {
media, initPosition, isMobile, title, isVisible, onClose
} = this.props;
const photos = media.filter(
item => typeof item.video === 'undefined'
);
const videos = media.filter(
item => typeof item.video !== 'undefined'
);
const initState = {
media: {
items: media,
filteredItems: {
all: media,
photos,
videos
},
filter: 'all',
initPosition,
currentPosition: initPosition
},
gallery: {
isMobile,
title
}
};
const store = createStore(
reducer,
initState,
composeEnhancers(applyMiddleware(...middleware))
);
return (
<Provider store={store}>
<App onClose={e => onClose(e)} isVisible={isVisible} />
</Provider>
);
I call my modal like this:
<Gallery
media={videos.concat(photos)}
isMobile={isMobile}
isVisible={show}
onClose={() => this.setState({ show: false })}
initPosition={position}
changePosition={position => this.setState({ position })}
title="Maximus"/>
And this is how I connect it to the state:
function mapStateToProps(state) {
const { media, gallery } = state;
const {
filteredItems, filter, currentPosition, initPosition
} = media;
const { isMobile, title } = gallery;
return {
filteredMedia: filteredItems,
filter,
currentPosition,
initPosition,
isMobile,
title
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
changeMediaProp
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(
GalleryApp
);
After isVisible is changed nothing seem to work with redux store. It is changing, but the app isn't updating.
When I toggle modal (change isVisible prop), redux state keeps changing, but my app isn't rerendering.
So to sum it up. I change isVisible and initPosition from surrounded application( these props are not stored in store), and when I changed them my component can't react to changes from reducer store.
I was passing multiple stores to my application. I fixed it by saving store in constructor and not creating it multiple times.
let newStore = store;
if (!newStore) {
newStore = createStore(
reducer,
initState,
composeEnhancers(applyMiddleware(...middleware))
);
this.setState({ store: newStore });
}
return (
<Provider store={newStore}>
<App onClose={e => onClose(e)} isVisible={isVisible} />
</Provider>
);
Does anyone have any better solution?
Your code doesn't have enough information to know. Are you using connect and react-redux. Here's a good intro if you need some help.
https://www.sohamkamani.com/blog/2017/03/31/react-redux-connect-explained/
An example component would look like this:
import { connect } from 'react-redux'
const TodoItem = ({ todo, destroyTodo }) => {
return (
<div>
{todo.text}
<span onClick={destroyTodo}> x </span>
</div>
)
}
const mapStateToProps = state => {
return {
todo: state.todos[0]
}
}
const mapDispatchToProps = dispatch => {
return {
destroyTodo: () =>
dispatch({
type: 'DESTROY_TODO'
})
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(TodoItem)
I have a redux-form component and another container component that load apollo graphql data. Here below just some important parts of code.
FORM COMPONENT:
class Form extends Component {
constructor(props) {
super(props);
setTimeout(function() {
this.executeCode ( 'onChangeInput', { action: 'initForm' , props: this.props, formProps: this.props, formState: this.state });
}.bind(this), 1000);
}
render() {
(...)
}
}
const ComponentWithData = reduxForm({
form: nameForm,
validate,
})(Form);
function mapStateToProps(state, ownProps) {
const log = false;
const statesReturn = { myState: state };
let initialValues;
initialValues = processValues(ownProps, tableCrud, ownProps.data, 'toClient','view' );
statesReturn.initialValues = initialValues ;
return statesReturn;
}
const ComponentWithDataAndState = connect(
mapStateToProps,
null,
)(ComponentWithData);
export default ComponentWithDataAndState;
CONTAINER COMPONENT:
class FormContainer extends Component {
render() {
const { t, ...otherProps} = this.props;
let aElements = [];
let aQlFiltered = {"crud_view_payment":{"table":"payment"}};
const resultCheck = checkLoadCrud (aQlFiltered,this.props);
if (resultCheck.messageError) {
return <MsgError msg={resultCheck.messageError} t={this.props.t} />;
}
if (!resultCheck.globalLoading && !resultCheck.messageError) {
if (this.props['crud_view_'+tableCrud] && this.props['crud_view_'+tableCrud][tableCrud]) {
if (this.props['crud_view_'+tableCrud][tableCrud].deleted) {
aElements.push(<RecordHeadInfo
key="recordhead"
tableCrud={tableCrud}
{...this.props}
data={this.props['crud_view_'+tableCrud][tableCrud]}
/>);
}
}
}
if (!resultCheck.globalLoading && !resultCheck.messageError) {
aElements.push(<Form
crudAction="View"
key="mainform"
id={ this.props.match.params.id }
data={this.props['crud_view_'+tableCrud][tableCrud]}
onSubmit={this.handleSubmit}
containerPropsForm={this.props}
t={this.props.t}
/>);
}
}
return (
<div>
{aElements}
</div>
);
}
}
const withGraphqlandRouter = compose(
graphql(defQls.payment.View, {
name: 'crud_view_payment',
options: props => {
const optionsValues = { variables: {id: props.match.params.id, _qlType: 'View' }};
optionsValues.fetchPolicy = Tables[tableCrud].fetchPolicy ? Tables[tableCrud].fetchPolicy :'network-only';
return optionsValues;
},
}),
)(withRouter(FormContainer));
const mapStateToProps = (state) => {
return {
myState: state,
};
};
const mapDispatchToProps = (dispatch) => {
return bindActionCreators ({ appSubmitStart, appSubmitStop, showConfirm, initialize, dispatch }, dispatch ); // to set this.props.dispatch
};
const withState = connect(
mapStateToProps ,
mapDispatchToProps,
)(withGraphqlandRouter);
const ComponentFull = withState;
export default ComponentFull;
on Form Component I have a setTimeout that execute a code, because I need set disable or hidden field accord to data loaded. I can't do it directly on construct() neither componentDidMount() because if i try to retrieve data from redux: this.props.myState.form ( have no values). it's for that use a timeout, with 1000 is ok, and with 1 milisecond it's ok too, I see this.props.myState.form.myForm.values (with data retrivied from db trough apollo), i prefer 1 milisecond of course because i don't see blink fields that get disabled or dissapears, but i'm not sure that is a good practice, because in a slow computer or slow browser that can produce conflict with the render ?
It's not clear form the lifecycle mixing react, redux, apollo and redux-form; anyone has idea how i can order better my ideas to write better code here?
Hi I'm new to Redux and I'm using React and Redux to try to build a UI where I can drag and drop files (invoices in this case) into a portion of the UI, render them in a list and then be able to launch a popover to edit the metadata associated with each invoice. Dragging and dropping is all working fine - Redux is re-rendering the view each time a file is dropped and the list is being updated. However, when I try an click the edit button against each invoice the store is being updated but the props in my popover component are not. Indeed, it doesn't look like any re-rendering is happening at all when I attempt to click the edit invoice button
App.js
import React from 'react'
import AddInvoice from '../containers/AddInvoice'
import CurrentInvoiceList from '../containers/CurrentInvoiceList'
import ControlPopover from '../containers/ControlPopover'
const App = () => (
<div>
<AddInvoice />
<CurrentInvoiceList />
<ControlPopover />
</div>
)
export default App
containers/AddInvoice.js
import React from 'react'
import { connect } from 'react-redux'
import { addInvoice } from '../actions'
const recipientDataDefaults = {
name: '',
surname: '',
address: '',
phone: ''
};
const handleDragOver = event => {
event.stopPropagation();
event.preventDefault();
event.dataTransfer.dropEffect = 'copy';
};
const handleDragEnter = event => {
event.stopPropagation();
event.preventDefault();
};
const handleDragLeave = event => {
event.stopPropagation();
event.preventDefault();
};
let AddInvoice = ({ dispatch }) =>
const styles = {'minHeight': '200px', 'background': 'tomato'}
return (
<div style={styles}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={event => {
event.stopPropagation();
event.preventDefault();
const data = event.dataTransfer;
const files = data.files;
const newInvoiceUploads = Object.keys(files)
.map(key => files[key])
.map(file => {
const invoiceObject = {};
invoiceObject.files = [file];
invoiceObject.recipientData = Object.assign({}, recipientDataDefaults);
return invoiceObject;
});
newInvoiceUploads.forEach(invoice => dispatch(addInvoice(invoice)))
}}>
Drag an invoice here to upload
</div>
)
}
AddInvoice = connect()(AddInvoice)
export default AddInvoice
containers/ControlPopover.js
import { connect } from 'react-redux'
import { closePopoverWithoutSave } from '../actions'
import Popover from '../components/Popover/Popover'
const mapStateToProps = (state) => {
return {
isActive: !!state.isActive
}
}
const mapDispatchToProps = {
handleCancel: closePopoverWithoutSave
}
const ControlPopover = connect(
mapStateToProps,
mapDispatchToProps
)(Popover)
export default ControlPopover
containers/CurrentInvoiceList.js
import { connect } from 'react-redux'
import { showInvoiceEditPopover } from '../actions'
import InvoiceList from '../components/InvoiceList/InvoiceList'
const mapStateToProps = state => {
return {
invoices: state.invoices
}
}
const mapDispatchToProps = dispatch => ({
handleEditInvoice: invoice => {
dispatch(showInvoiceEditPopover(invoice))
}
})
const CurrentInvoiceList = connect(
mapStateToProps,
mapDispatchToProps
)(InvoiceList)
export default CurrentInvoiceList
actions/index.js
let nextInvoiceId = 0
export const addInvoice = invoice => ({
type: 'ADD_INVOICE',
id: nextInvoiceId++,
invoiceData: invoice
})
export const showInvoiceEditPopover = invoice => ({
type: 'SHOW_POPOVER',
invoice
})
The popover reducer (combined in app but inlined here for brevity) reducers/index.js
const popover = (state = {}, action) => {
switch (action.type) {
case 'SHOW_POPOVER':
const popoverState = {}
popoverState.isActive = true
popoverState.data = action.invoice
return popoverState
case 'CLOSE_POPOVER_WITHOUT_SAVING':
const inactiveState = {}
inactiveState.isActive = false
inactiveState.data = {}
return inactiveState;
default:
return state
}
}
export default popover
components/InvoiceList.js
import React from 'react'
import PropTypes from 'prop-types'
import Invoice from '../Invoice/Invoice'
const InvoiceList = ({ invoices, handleEditInvoice }) => {
return (
<div>
{invoices.map(invoice =>
<Invoice
key={invoice.id}
invoice={invoice.invoiceData}
onClick={event => {
// here we invoke the action bound by the CurrentInvoiceList
// container
event.preventDefault()
handleEditInvoice(invoice)
}}
/>
)}
</div>
)
}
InvoiceList.propTypes = {
invoices: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
invoiceData: PropTypes.object
}).isRequired).isRequired,
handleEditInvoice: PropTypes.func.isRequired
}
export default InvoiceList
components/Invoice.js
import React from 'react'
import PropTypes from 'prop-types'
import TextInput from '../TextInput/TextInput';
import Button from '../Button/Button';
import './invoice.css'
const Invoice = ({ invoice, onClick }) => {
const fileNames = invoice.files.map((file, index) => {
return (<div key={index} className="invoice__file-title-legend">
{file.name}</div>);
});
return (
<div className="invoice">
<form className="invoice__form">
<TextInput id="invoice__input-amount" placeholder="enter invoice amount" label="Invoice Amount" />
<TextInput id="invoice__input-target" placeholder="enter payment target" label="Payment Target" />
<Button value="Add recipient" onClick={onClick} /> // clicking this button updates the store but does NOT re-render. Why?
</form>
<div className="invoice__files">{fileNames}</div>
</div>
)
}
Invoice.propTypes = {
onClick: PropTypes.func.isRequired,
invoice: PropTypes.object
}
export default Invoice
components/Popover/Popover.js
import React from 'react'
import PropTypes from 'prop-types'
import createModifiers from '../../lib/createModifiers';
import './Popover.css';
const Popover = ({ handleCancel, isActive }) => {
console.log('rendering popover component') // does not get called when invoice edit button is pressed
const popoverModifiers = createModifiers('popover', {
'is-active': isActive
})
return (
<div className={popoverModifiers}>
<div className="popover__header">
<button onClick={handleCancel}>x</button>
</div>
<div className="popover__content">
Popover content
</div>
</div>
)
}
Popover.propTypes = {
handleCancel: PropTypes.func.isRequired,
isActive: PropTypes.bool.isRequired
}
export default Popover
And finally the createModifiers Code for posterity. This code is merely producing some BEM modifier CSS classes based on a state boolean value passed in
const createModifiers = (element, modifiers) => {
const classList = Object.keys(modifiers)
.filter(key => modifiers[key])
.map(modifier => `${element}--${modifier}`)
.concat(element)
.join(' ')
return classList
}
export default createModifiers
I know this is a large amount of example code so I tried to keep it a brief and focused as possible whilst giving a comprehensive view of the application. Any help is most appreciated.
The problem is in containers/ControlPopover.js and the mapStateToProps function. The isActive property needs to be assigned to state.popover.isActive
I believe your problem is your mapDispatchToProp functions are not formatted properly.
You need to return an object that has methods. Those methods are what will be given to your connected component as props.
Example:
const mapDispatchToProps = ( dispatch ) => {
return {
doSomething: ( arguments ) => {
// here you can dispatch and use your arguments
}
};
}
doSomething is the prop that would be provided to the connected component.
All of your mapDispatchToProps functions are formatted improperly.
SIDE NOTE / OPINION - TLDR:
In the future if you have a lot of code to post, I believe it would be easier to digest if the pieces were linked together.
I.E.
// App.js
const App = () => (
<div>
<Header />
<Body />
<Footer />
</div>
);
The components appear in the order: header -> body -> footer. Provide the code for them in that order, with their actions, reducer, presentational, and container information in one block.
Header
// header.presentational.js ...
// header.container.js ... ( or where you mapStateToProps and connect )
// header.actions.js ...
// header.reducer.js ...
Body ...
Footer ...
I don't know if the code is different on your end, but your mapStateToDispatch function is still improperly formatted.
Change this...
const mapDispatchToProps = dispatch => ({
handleEditInvoice: invoice => {
dispatch(showInvoiceEditPopover(invoice))
}
})
To this:
const mapDispatchToProps = dispatch => ({
return {
handleEditInvoice: invoice => {
dispatch(showInvoiceEditPopover(invoice))
}
};
})
I am using react and redux.
I have a Container component defined as so:
import { connect } from 'react-redux';
import {addTag} from 'actions';
import ExpenseTagsControl from './expense_tags_control'
const mapStateToProps = (state, own_props={selected_tags:[]}) => {
return {
tags_list: state.tags.tags_list
};
};
const mapDispatchToProps = (dispatch) => {
return {
addTag: (tag_name) => {
dispatch(addTag(tag_name))
}
};
};
const AddExpenseTagsContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ExpenseTagsControl);
export default AddExpenseTagsContainer;
The container wraps a presentational component which is defined as so:
// expense_tags_control.js
import React, {Component, PropTypes} from 'react';
import ChipInput from 'material-ui-chip-input';
import Chip from 'material-ui/Chip';
import Avatar from 'material-ui/Avatar';
import Tag from 'common/svg_icons/tag';
import AutoComplete from 'material-ui/AutoComplete'
import _ from 'underscore';
class ExpenseTagsControl extends React.Component {
constructor(props) {
super(props);
this.state = {
chips: []
};
};
handleAdd(chip) {
// If the chip does not already exist, add it. the id here will be a dummy value that is not there in the tags_list
if (!(_.contains( _.map(this.props.tags_list, (tag) => tag.id), chip.id))) {
this.props.addTag(chip.name);
}
// This is wrong.
this.setState({
chips: [...this.state.chips, chip]
});
};
handleDelete(chip) {
this.setState({
chips: this.state.chips.filter((c) => c !== deletedChip)
});
};
chipRenderer({ text, value, isFocused, isDisabled, handleClick, handleRequestDelete }, key) {
const style = {
margin: '8px 8px 0 0',
float: 'left',
pointerEvents: isDisabled ? 'none' : undefined
};
return (
<Chip key={key} style={style} onTouchTap={handleClick} onRequestDelete={handleRequestDelete}>
<Avatar size={24} icon={<Tag />} />
{text}
</Chip>
);
};
render() {
return (
<ChipInput
hintText="Tags"
value={this.state.chips}
onRequestAdd={(chip) => this.handleAdd(chip)}
onRequestDelete={(deletedChip) => this.handleDelete(deletedChip)}
fullWidth={true}
dataSourceConfig={{ text: 'name', value: 'id' }}
dataSource={this.props.tags_list}
chipRenderer={this.chipRenderer}
openOnFocus={false}
filter={AutoComplete.fuzzyFilter}
onRequestDelete={console.log("Deleted")}
/>);
};
};
ExpenseTagsControl.PropTypes = {
tags_list: PropTypes.array.isRequired,
addTag: PropTypes.func.isRequired,
value: PropTypes.array.isRequired,
onChange: PropTypes.func.isRequired
};
export default ExpenseTagsControl;
The presentational component above, maintains a state, which indicates the chips that have been selected.
The ChipInput component allows you to select chips which are objects with an id, and a name, defined from a pre-existing data source. The component also allows you to add a new chip by typing in the name. If the typed in name does not exist in the data source, it is added to the data source.
My Problem
The id of the newly added chip is assigned once the addTag() action is dispatched. How do I get the value of the result of the action that was just dispatched?
I thought about working around this by maintaining the state of the ChipInput in the global state, and manipulate the global state upon dispatching the addTag() action. But that feels like too much overhead.
If what I understand is correct, you might want something like this:
class ExpenseTagsControl extends React.Component {
// ...
/*
* assuming your reducers are working fine and 'addTag'
* has updated global 'state.tags.tags_list'
*/
componentWillReceiveProps(nextProps) {
this.setState({ chips: this.nextProps.tags_list });
}
// ...
}
NB: You might need to optimize calling setState inside componentWillReceiveProps based on some conditions to avoid unnecessary re-render.
From what I understand, the OP's problem is how to dispatch an action to modify the redux store and at the same time update the component's local state.
Edit: added a working example
const initialState = {
tags: ['hello', 'hi', 'howdy']
}
function reducer(state = {}, action) {
switch (action.type) {
case 'ADD_TAG':
return {
...state,
tags: [
...state.tags,
action.payload.tag
]
}
default:
return state;
}
}
const store = Redux.createStore(reducer, initialState);
const addTag = (tag) => ({
type: 'ADD_TAG',
payload: {
tag
}
})
class Chips extends React.Component {
constructor(props) {
super(props);
this.chipToAdd = false;
this.state = {
chips: []
}
this.handleAdd = this.handleAdd.bind(this);
}
componentWillReceiveProps(nextProps) {
console.log(this.chipToAdd);
if (this.chipToAdd) {
this.setState({
chips: [...this.state.chips, this.chipToAdd]
}, (this.chipToAdd = false));
}
}
handleAdd(chip) {
if (this.props.tags.filter(tag => tag === chip).length === 0) {
this.chipToAdd = chip;
this.props.addTag(chip);
} else {
if (this.state.chips.filter(existingChip => existingChip === chip).length === 0) {
this.setState({
chips: [...this.state.chips, chip]
});
}
}
}
render() {
return <div >
< h3 > Tags added in component 's chip state</h3>
<ul>
{this.state.chips.map((chip, index) => <li key={index}>{chip}</li>)}
</ul>
<hr />
<h3>Tags in Redux Store</h3>
{this.props.tags.map(
(tag, index) => <li key={index}>
{tag} <button onClick={() => this.handleAdd(tag)}>Add</button>
</li>
)}
<button onClick={() => this.handleAdd('
new tag - ' + Math.floor((Math.random() * 100) + 1))}>Add a chip with new tag</button>
</div>
}
}
const mapStateToProps = ({ tags = [] }) => ({ tags });
const ConnectedChips = ReactRedux.connect(mapStateToProps, { addTag })(Chips);
class App extends React.Component {
render() {
return <div>
<h1>React/Redux Demo</h1>
<ConnectedChips />
</div>
}
}
const Provider = ReactRedux.Provider;
ReactDOM.render(
<Provider store={store}><App /></Provider>,
document.getElementById('
root ')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<script src="https://unpkg.com/redux#3.6.0/dist/redux.min.js"></script>
<script src="https://unpkg.com/react-redux#4.4.6/dist/react-redux.min.js"></script>
<div id="root"></div>