React child component state management - reactjs

I´ve been reading about different ways of managing state from child components but I´m still a little confused if my workflow is correct or if it´s an anti-pattern.
Basically I have a component with material-ui sibling components and those component´s props need to be update as their state changes. Is there a better way of managing child components state ?
import RaisedButton from 'material-ui/RaisedButton';
import TextField from 'material-ui/TextField';
import React, { Component } from 'react';
import './SearchTicket.css';
class SearchTicket extends Component {
constructor(props) {
super(props);
this.state = {
txtSearchTicket: {
errorText: null,
value: '',
hintText: 'Número do chamado',
},
btnSearch: {
label: 'Buscar',
disabled: false,
},
};
this.handleTxtSearchTicket = this.handleTxtSearchTicket.bind(this);
this.searchTicket = this.searchTicket.bind(this);
this.isTicketValid = this.isTicketValid.bind(this);
}
handleTxtSearchTicket(e) {
const value = e.target.value;
this.setState((prevState) => {
const newState = prevState;
newState.txtSearchTicket.value = value;
return newState;
});
}
isTicketValid() {
const re = new RegExp(/^(im|sd|[0-9])[0-9]+/, 'i');
if (re.test(this.state.txtSearchTicket.value)) {
return true;
}
this.setState((prevState) => {
const newState = prevState;
newState.txtSearchTicket.value = '';
newState.txtSearchTicket.errorText = 'Número de chamado inválido';
return newState;
});
}
searchTicket() {
if (this.isTicketValid()) {
console.log(`Fetching call ${this.state.txtSearchTicket.value}`);
this.setState((prevState) => {
const newState = prevState;
newState.btnSearch.disabled = !newState.btnSearch.disabled;
return newState;
});
}
// TODO: implement retrieve call from api
}
render() {
const style = {
margin: 15,
width: 180,
heigh: 40,
};
return (
<form>
<TextField
name="txtSearchTicket"
hintText={this.state.txtSearchTicket.hintText}
floatingLabelText={this.state.txtSearchTicket.hintText}
errorText={this.state.txtSearchTicket.errorText}
onChange={this.handleTxtSearchTicket}
value={this.state.txtSearchTicket.value}
/>
<br />
<RaisedButton
name="btnSearch"
label={this.state.btnSearch.label}
disabled={this.state.btnSearch.disabled}
onClick={() => this.searchTicket()}
style={style}
primary
/>
</form>
);
// <RaisedButton label="Buscar" style={style} primary={true}/>
}
}

Related

How to update prop values in Child class component when Parent class component state is changed? : React Native

I have a parent class component called CardView.js which contains a child class component called Tab.js (which contains a FlatList).
When a button is pressed in CardView.js, a modal appears with various options. A user chooses an option and presses 'OK' on the modal. At this point the onOKHandler method in the parent component updates the parent state (tabOrderBy: orderBy and orderSetByModal: true). NOTE: These two pieces of state are passed to the child component as props.
Here is what I need:
When the onOKHandler is pressed in the parent, I need the child component to re-render with it's props values reflecting the new state values in the parent state. NOTE: I do not want the Parent Component to re-render as well.
At the moment when onOKHandler is pressed, the child component reloads, but it's props are still showing the OLD state from the parent.
Here is what I have tried:
When the onOKHandler is pressed, I use setState to update the parent state and then I use the setState callback to call a method in the child to reload the child. The child reloads but its props are not updated.
I have tried using componentDidUpdate in the child which checks when the prop orderSetByModal is changed. This does not work at all.
I have tried many of the recommendations in other posts like this - nothing works! Where am I going wrong please? Code is below:
Parent Component: CardView.js
import React from "react";
import { View } from "react-native";
import { Windows} from "../stores";
import { TabView, SceneMap } from "react-native-tab-view";
import { Tab, TabBar, Sortby } from "../components";
class CardView extends React.Component {
state = {
level: 0,
tabIndex: 0,
tabRoutes: [],
recordId: null,
renderScene: () => {},
showSortby: false,
orderSetByModal: false,
tabOrderBy: ''
};
tabRefs = {};
componentDidMount = () => {
this.reload(this.props.windowId, null, this.state.level, this.state.tabIndex);
};
reload = (windowId, recordId, level, tabIndex) => {
this.setState({ recordId, level, tabIndex });
const tabRoutes = Windows.getTabRoutes(windowId, level);
this.setState({ tabRoutes });
const sceneMap = {};
this.setState({ renderScene: SceneMap(sceneMap)});
for (let i = 0; i < tabRoutes.length; i++) {
const tabRoute = tabRoutes[i];
sceneMap[tabRoute.key] = () => {
return (
<Tab
onRef={(ref) => (this.child = ref)}
ref={(tab) => (this.tabRefs[i] = tab)}
windowId={windowId}
tabSequence={tabRoute.key}
tabLevel={level}
tabKey={tabRoute.key}
recordId={recordId}
orderSetByModal={this.state.orderSetByModal}
tabOrderBy={this.state.tabOrderBy}
></Tab>
);
};
}
};
startSortByHandler = () => {
this.setState({showSortby: true});
};
endSortByHandler = () => {
this.setState({ showSortby: false});
};
orderByFromModal = () => {
return 'creationDate asc'
}
refreshTab = () => {
this.orderByFromModal();
this.child.refresh()
}
onOKHandler = () => {
this.endSortByHandler();
const orderBy = this.orderByFromModal();
this.setState({
tabOrderBy: orderBy,
orderSetByModal: true}, () => {
this.refreshTab()
});
}
render() {
return (
<View>
<TabView
navigationState={{index: this.state.tabIndex, routes: this.state.tabRoutes}}
renderScene={this.state.renderScene}
onIndexChange={(index) => {
this.setState({ tabIndex: index });
}}
lazy
swipeEnabled={false}
renderTabBar={(props) => <TabBar {...props} />}
/>
<Sortby
visible={this.state.showSortby}
onCancel={this.endSortByHandler}
onOK={this.onOKHandler}
></Sortby>
</View>
);
}
}
export default CardView;
Child Component: Tab.js
import React from "react";
import { FlatList } from "react-native";
import { Windows } from "../stores";
import SwipeableCard from "./SwipeableCard";
class Tab extends React.Component {
constructor(props) {
super(props);
this.state = {
currentTab: null,
records: [],
refreshing: false,
};
this.listRef = null;
}
async componentDidMount() {
this.props.onRef(this);
await this.reload(this.props.recordId, this.props.tabLevel, this.props.tabSequence);
}
componentWillUnmount() {
this.props.onRef(null);
}
//I tried adding componentDidUpdate, but it did not work at all
componentDidUpdate(prevProps) {
if (this.props.orderSetByModal !== prevProps.orderSetByModal) {
this.refresh();
}
}
getOrderBy = () => {
let orderByClause;
if (this.props.orderSetByModal) {
orderByClause = this.props.tabOrderBy;
} else {
orderByClause = "organization desc";
}
return orderByClause;
};
async reload() {
const currentTab = Windows.getTab(this.props.windowId, this.props.tabSequence, this.props.tabLevel);
this.setState({ currentTab });
let response = null;
const orderBy = this.getOrderBy();
response = await this.props.entity.api.obtainRange(orderBy);
this.setState({ records: response.dataList })
}
refresh = () => {
this.setState({ refreshing: true }, () => {
this.reload(this.props.recordId, this.props.tabLevel, this.props.tabSequence)
.then(() => this.setState({ refreshing: false }));
});
};
renderTabItem = ({ item, index }) => (
<SwipeableCard
title={"Card"}
/>
);
render() {
if (!this.state.currentTab) {
return null;
}
return (
<>
<FlatList
ref={(ref) => (this.listRef = ref)}
style={{ paddingTop: 8 }}
refreshing={this.state.refreshing}
onRefresh={this.refresh}
data={this.state.records}
keyExtractor={(item) => (item.isNew ? "new" : item.id)}
/>
</>
);
}
}
export default Tab;

Import a callback from a class to an other in typescript

I'am using typescript 3.
I have this map component :
import * as React from 'react';
import GoogleMapReact from 'google-map-react';
import { any } from 'prop-types';
import {Coords} from 'google-map-react';
export interface HelloProps { center: Coords ; zoom: number ; }
const AnyReactComponent = ( {text} : { lat: any,text:any,lng:any}) => <div>{text}</div>;
export class SimpleMap extends React.Component <HelloProps, {}> {
defaultProps = {
center: {
lat: 59.95,
lng: 30.33
},
zoom: 11
};
myCallbackMap = (dataFromChild:number) => {
this.defaultProps.zoom = dataFromChild;
}
render() {
return (
// Important! Always set the container height explicitly
<div style={{ height: '100vh', width: '100%' }}>
<GoogleMapReact
bootstrapURLKeys={{ key: 'AIzaSyDs5u7u1Almh8-Jch3cHzuWB-egVPTZajs' }}
defaultCenter={this.props.center}
defaultZoom={this.props.zoom}
>
<AnyReactComponent
lat={59.955413}
lng={30.337844}
text="My Marker"
/>
</GoogleMapReact>
</div>
);
}
}
export default SimpleMap;
And i want to use :
myCallbackMap = (dataFromChild:number) => {
this.defaultProps.zoom = dataFromChild;
}
In my search bar so when someone search it change the map.
Here is my search file (scroll down to see the call) :
import * as React from 'react';
import axios from 'axios'
import Suggestions from './Suggestions'
import myCallbackMap from './Map'
const API_KEY:string = "process.env"
const API_URL:string = 'http://127.0.0.1:9001/v1/test'
export class Search extends React.Component{
state = {
query: '' as string,
results : [] as any[]
}
search = {
value: '' as string,
}
getInfo = () => {
axios.post(`${API_URL}/${this.state.query}/`)
.then(({ data }) => {
this.setState({
results: data.data
})
})
}
handleInputChange = () => {
this.setState({
query: this.search.value
}, () => {
if (this.state.query && this.state.query.length > 1) {
if (this.state.query.length % 2 === 0) {
this.getInfo()
}
} else if (!this.state.query) {
}
})
}
myCallbackSearch = (dataFromChild:string) => {
this.search.value = dataFromChild;
// here where i want ot use the map call back
myCallbackMap(111);
}
render() {
return (
<form>
<input
placeholder="Search for..."
ref={input => this.search = input}
onChange={this.handleInputChange}
/>
<Suggestions results={this.state.results} callbackFromParentSearch={this.myCallbackSearch}/>
</form>
)
}
}
export default Search
How to use and import properly the map call back and use it in the search component ? The main goal is that when someone type in the search bar it change the map dynamicaly. but now i'am working on a simple prototype.
Regards
You cannot import it because it would be a static function when you need an instance function.
You have to pass the callback as a prop of the search component.
The callback itself actually looks like an event handler which role is to change the default props (which should be declared static) of the google map in another component. So my guess is you don't actually want to change the default props of the map but rather give it a new zoom value via props.
You can either:
Implement the handler in the common ancestor to both the search and the map component, make the zoom a state of this common ancestor, update this state in the callback, and propagate through props the callback all the way to the search component, and the zoom value all the way to the map component.
Or:
Put this state in a redux store which will allow you to skip the props chaining and directly register map to listen for zoom change in the store, and pass an action creator to the search component so that it can notify the store when the search change.
Common ancestor version:
import * as React from 'react';
import axios from 'axios';
import Suggestions from './Suggestions'
const API_KEY:string = "process.env"
const API_URL:string = 'http://127.0.0.1:9001/v1/test'
export class Search extends React.Component{
state = {
query: '' as string,
results : [] as any[]
}
search = {
value: '' as string,
}
getInfo = () => {
axios.post(`${API_URL}/${this.state.query}/`)
.then(({ data }) => {
this.setState({
results: data.data
})
})
}
handleInputChange = () => {
this.setState({
query: this.search.value
}, () => {
if (this.state.query && this.state.query.length > 1) {
if (this.state.query.length % 2 === 0) {
this.getInfo()
}
} else if (!this.state.query) {
}
})
}
myCallbackSearch = (dataFromChild:string) => {
this.search.value = dataFromChild;
// here where i want ot use the map call back
myCallbackMap(111);
}
render() {
const { callbackFromParentSearch } = this.props;
return (
<form>
<input
placeholder="Search for..."
ref={input => this.search = input}
onChange={this.handleInputChange}
/>
<Suggestions results={this.state.results} callbackFromParentSearch={callbackFromParentSearch}/>
</form>
)
}
}
export default Search;
Note that the components don't need to have the same exact parent below is just an example. It will still work if you pass the props from parent to child until you get to the targets Search and SimpleMap.
export default class CommonAncestor extends Component {
state: {
zoom: 3 as number,
}
handleCallbackFromParentSearch = (dataFromChild:number) => {
this.setState(() => ({ zoom: dataFromChild }));
}
render() {
const { zoom } = this.state;
return (
<React.Fragment>
<Search callbackFromParentSearch={this.handleCallbackFromParentSearch} />
<SimpleMap zoom={zoom} />
</React.Fragment>
);
}
}
Redux version:
Search component connected to the changeZoom action
import * as React from 'react';
import axios from 'axios';
import { connect } from 'react-redux';
import Suggestions from './Suggestions';
import { actionCreators as zoomAC } from 'src/redux/zoom';
const API_KEY:string = "process.env"
const API_URL:string = 'http://127.0.0.1:9001/v1/test'
class Search extends React.Component{
state = {
query: '' as string,
results : [] as any[]
}
search = {
value: '' as string,
}
getInfo = () => {
axios.post(`${API_URL}/${this.state.query}/`)
.then(({ data }) => {
this.setState({
results: data.data
})
})
}
handleInputChange = () => {
this.setState({
query: this.search.value
}, () => {
if (this.state.query && this.state.query.length > 1) {
if (this.state.query.length % 2 === 0) {
this.getInfo()
}
} else if (!this.state.query) {
}
})
}
myCallbackSearch = (dataFromChild:string) => {
this.search.value = dataFromChild;
// here where i want ot use the map call back
myCallbackMap(111);
}
render() {
const { changeZoom} = this.props;
return (
<form>
<input
placeholder="Search for..."
ref={input => this.search = input}
onChange={this.handleInputChange}
/>
<Suggestions results={this.state.results} callbackFromParentSearch={changeZoom}/>
</form>
)
}
}
export default connect(
null,
dispatch => ({
changeZoom: value => dispatch(zoomAC.changeZoom(value))
})
)(Search);
SimpleMap component connected to the zoom store:
import * as React from 'react';
import GoogleMapReact from 'google-map-react';
import { connect } from 'react-redux';
import { any } from 'prop-types';
import {Coords} from 'google-map-react';
export interface HelloProps { center: Coords ; zoom: number ; }
const AnyReactComponent = ( {text} : { lat: any,text:any,lng:any}) => <div>{text}</div>;
export class SimpleMap extends React.Component <HelloProps, {}> {
defaultProps = {
center: {
lat: 59.95,
lng: 30.33
},
zoom: 11
};
render() {
return (
// Important! Always set the container height explicitly
<div style={{ height: '100vh', width: '100%' }}>
<GoogleMapReact
bootstrapURLKeys={{ key: 'AIzaSyDs5u7u1Almh8-Jch3cHzuWB-egVPTZajs' }}
defaultCenter={this.props.center}
defaultZoom={this.props.zoom}
>
<AnyReactComponent
lat={59.955413}
lng={30.337844}
text="My Marker"
/>
</GoogleMapReact>
</div>
);
}
}
export default connect(
state => ({ zoom: state.zoom })
)(SimpleMap);

Lot of repetition in React component

I have a rather large React component that manages the display of a detail for a job on my site.
There are a few things that I would like to do smarter
The component has a few options for opening Dialogs. For each dialog I have a separate Open and Close function. For example handleImageGridShow and handleImageGridClose. Is there any way to be more concise around this?
I have many presentational components (e.g. ViewJobDetails) that shows details about the job. My issue is that I have to pass them down into each Component as a prop and I'm passing the same props over and over again
As I'm loading my data from firebase I often have to do similar checks to see if the data exists before I render the component (e.g.this.state.selectedImageGrid && <ImageGridDialog />). Is there any more clever way of going about this?
import React, { Component } from 'react';
import { withStyles } from 'material-ui/styles';
import ViewJobAttachment from "../../components/jobs/viewJobAttachment";
import ViewJobDetails from "../../components/jobs/viewJob/viewJobDetails";
import ViewJobActions from "../../components/jobs/viewJob/viewJobActions";
import ViewCompanyDetails from "../../components/jobs/viewJob/viewCompanyDetails";
import ViewClientsDetails from "../../components/jobs/viewJob/viewClientsDetails";
import ViewProductsDetails from "../../components/jobs/viewJob/viewProductsDetails";
import ViewAttachmentDetails from "../../components/jobs/viewJob/viewAttachmentDetails";
import ViewEventLogDetails from "../../components/jobs/viewJob/viewEventLogDetails";
import ViewSummaryDetails from "../../components/jobs/viewJob/viewSummary";
import {FirebaseList} from "../../utils/firebase/firebaseList";
import SimpleSnackbar from "../../components/shared/snackbar";
import {calculateTotalPerProduct} from "../../utils/jobsService";
import BasicDialog from "../../components/shared/dialog";
import ImageGrid from "../../components/shared/imageGrid";
import Spinner from "../../components/shared/spinner";
import ViewPinnedImageDialog from "../../components/jobs/viewEntry/viewPinnedImage";
import {
Redirect
} from 'react-router-dom';
const styles = theme => ({
wrapper: {
marginBottom: theme.spacing.unit*2
},
rightElement: {
float: 'right'
}
});
const ImageGridDialog = (props) => {
return (
<BasicDialog open={!!props.selectedImageGrid}
handleRequestClose={props.handleRequestClose}
fullScreen={props.fullScreen}
title={props.title}
>
<ImageGrid selectedUploads={props.selectedImageGrid}
handleClickOpen={props.handleClickOpen}/>
</BasicDialog>
)
};
class ViewJob extends Component {
constructor() {
super();
this.state = {
currentJob: null,
entries: [],
promiseResolved: false,
attachmentDialogOpen: false,
openAttachment: null,
selectedImageGrid: false,
selectedPinnedImage: false,
showSnackbar: false,
snackbarMsg: '',
markedImageLoaded: false,
loading: true,
redirect: false
};
this.firebase = new FirebaseList('jobs');
this.handleJobStatusChange = this.handleJobStatusChange.bind(this);
this.handleImageGridShow = this.handleImageGridShow.bind(this);
this.handleImageGridClose = this.handleImageGridClose.bind(this);
this.handlePinnedImageClose = this.handlePinnedImageClose.bind(this);
this.handlePinnedImageShow = this.handlePinnedImageShow.bind(this);
this.handleMarkedImageLoaded = this.handleMarkedImageLoaded.bind(this);
this.handleRemove = this.handleRemove.bind(this);
this.pushLiveToClient = this.pushLiveToClient.bind(this);
}
componentDidMount() {
this.firebase.db().ref(`jobs/${this.props.id}`).on('value', (snap) => {
const job = {
id: snap.key,
...snap.val()
};
this.setState({
currentJob: job,
loading: false
})
});
const previousEntries = this.state.entries;
this.firebase.db().ref(`entries/${this.props.id}`).on('child_added', snap => {
previousEntries.push({
id: snap.key,
...snap.val()
});
this.setState({
entries: previousEntries
})
});
}
handleRemove() {
this.firebase.remove(this.props.id)
.then(() => {
this.setState({redirect: true})
})
};
pushLiveToClient() {
const updatedJob = {
...this.state.currentJob,
'lastPushedToClient': Date.now()
};
this.firebase.update(this.state.currentJob.id, updatedJob)
.then(() => this.handleSnackbarShow("Job pushed live to client"))
}
handleJobStatusChange() {
const newState = !this.state.currentJob.completed;
const updatedJob = {
...this.state.currentJob,
'completed': newState
};
this.firebase.update(this.state.currentJob.id, updatedJob)
}
handleSnackbarShow = (msg) => {
this.setState({
showSnackbar: true,
snackbarMsg: msg
});
};
handleSnackbarClose= (event, reason) => {
if (reason === 'clickaway') {
return;
}
this.setState({ showSnackbar: false });
};
handleAttachmentDialogClose =() => {
this.setState({attachmentDialogOpen: false})
};
handleClickOpen = (file) => {
this.setState({
attachmentDialogOpen: true,
openAttachment: file
});
};
handleImageGridShow(imageGrid) {
this.setState({selectedImageGrid: imageGrid})
}
handleImageGridClose() {
this.setState({selectedImageGrid: false})
}
handlePinnedImageShow(pinnedImage) {
this.setState({selectedPinnedImage: pinnedImage})
}
handlePinnedImageClose() {
this.setState({selectedPinnedImage: false})
}
handleMarkedImageLoaded() {
this.setState({markedImageLoaded: true})
}
render() {
const {classes} = this.props;
let {_, costPerItem} = calculateTotalPerProduct(this.state.entries);
if (this.state.redirect) {
return <Redirect to='/jobs' push/>
} else {
if (this.state.loading) {
return <Spinner/>
} else {
return (
<div className={styles.wrapper}>
{this.state.currentJob &&
<div>
<ViewJobActions currentJob={this.state.currentJob}
handleJobStatusChange={this.handleJobStatusChange}
pushLiveToClient={this.pushLiveToClient}
/>
<ViewJobDetails currentJob={this.state.currentJob}/>
<ViewCompanyDetails currentJob={this.state.currentJob}/>
<ViewClientsDetails currentJob={this.state.currentJob}/>
<ViewProductsDetails currentJob={this.state.currentJob}/>
{this.state.currentJob.selectedUploads && this.state.currentJob.selectedUploads.length > 0
? <ViewAttachmentDetails currentJob={this.state.currentJob} handleClickOpen={this.handleClickOpen}/>
: null}
<ViewEventLogDetails jobId={this.state.currentJob.jobId}
jobKey={this.state.currentJob.id}
entries={this.state.entries}
handlePinnedImageShow={this.handlePinnedImageShow}
handleImageGridShow={this.handleImageGridShow}/>
<ViewSummaryDetails stats={costPerItem}/>
<ViewJobAttachment open={this.state.attachmentDialogOpen}
handleRequestClose={this.handleAttachmentDialogClose}
attachment={this.state.openAttachment}
/>
{this.state.selectedImageGrid &&
<ImageGridDialog selectedImageGrid={this.state.selectedImageGrid}
handleRequestClose={this.handleImageGridClose}
handleClickOpen={this.handleClickOpen}
title="Pictures for job"
fullScreen={false}/>}
{this.state.selectedPinnedImage &&
<ViewPinnedImageDialog attachment={this.state.selectedPinnedImage}
open={!!this.state.selectedPinnedImage}
markedImageLoaded={this.state.markedImageLoaded}
handleMarkedImageLoaded={this.handleMarkedImageLoaded}
handleRequestClose={this.handlePinnedImageClose}
otherMarkedEntries={this.state.entries}
/>
}
<SimpleSnackbar showSnackbar={this.state.showSnackbar}
handleSnackbarClose={this.handleSnackbarClose}
snackbarMsg={this.state.snackbarMsg}/>
</div>}
</div>
);
}
}
}
}
export default withStyles(styles)(ViewJob);
You can define a regular component method and bind it in handler like this onSomething={this.handler.bind(this, index)} assuming you have some distinguishable thing in the index var
function should look like this
handler(index) {
...
}

Getting the updated state after an action is dispatched in redux

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>

How to recover state on back button with react

If I have a simple react component that records a click count for a button and on each click records a new history state without changing the URL. When the user clicks back how do I restore the state to as it was?
I can do as it is here using the native JavaScript history object, but it fails when the user transitions back to the first state and back from a different component into the last state of this one.
I suspect that there is a better to do this using react-router (1.0)?
import React, { Component } from 'react';
export default class Foo extends Component {
state = {
clickCount: 0,
};
componentWillMount() {
window.onpopstate = (event) => {
if (event.state.clickCount) {
this.setState({ clickCount: event.state.clickCount });
}
};
}
onClick() {
const newClickCount = this.state.clickCount + 1;
const newState = { clickCount: newClickCount };
this.setState(newState);
history.pushState(newState, '');
}
render() {
return (
<div>
<button onClick={this.onClick.bind(this)}>Click me</button>
<div>Clicked {this.state.clickCount} times</div>
</div>
);
}
}
localStorage or even cookies are options, but probably not the best way. You should store the count in a database, this way you can set the initial state in your constructor to the last value saved in the database.
Another option, if you only need to persist the count on the client-side(and not in a database) is using a closure.
// CountStore.js
var CountStore = (function() {
var count = 0;
var incrementCount = function() {
count += 1;
return count;
};
var getCount = function() {
return count;
};
return {
incrementCount: incrementCount,
getCount: getCount
}
})();
export default CountStore;
So your code would change to the below.
import React, { Component } from 'react';
import CountStore from './CountStore';
export default class Foo extends Component {
state = {
clickCount: CountStore.getCount()
};
componentWillMount() {
window.onpopstate = (event) => {
if (event.state.clickCount) {
this.setState({ clickCount: event.state.clickCount });
}
};
}
onClick() {
const newClickCount = CountStore.incrementCount();
const newState = { clickCount: newClickCount };
this.setState(newState);
}
render() {
return (
<div>
<button onClick={this.onClick.bind(this)}>Click me</button>
<div>Clicked {this.state.clickCount} times</div>
</div>
);
}
}
There may be a cleaner way of using react-router, but this is an option.
An example:
import React, {Component} from "react";
import {NavLink} from "react-router-dom";
interface Props {
}
interface State {
count: number
}
export default class About extends Component<Props, State> {
UNSAFE_componentWillMount(): void {
this.setState(Object.getPrototypeOf(this).constructor.STATE || {});
}
componentWillUnmount(): void {
Object.getPrototypeOf(this).constructor.STATE = this.state;
}
constructor(props: Props) {
super(props);
this.state = {count: 0}
}
render() {
const {count} = this.state;
return <div style={{width: "100%", height: "100%", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "space-evenly", fontSize: "2em"}}>
<span>Count: {count}</span>
<button onClick={() => this.setState({count: count + 1})}>PLUS ONE</button>
<NavLink to="/">Redirect to HOME</NavLink>
</div>
}
}

Resources