Good day; I’m in need of assistance with the following problem:
A React class component (extending React.Component
) exposes a method to set it’s state on command. I know that using a prop on the component instead could probably solve this problem, but this public method is extremely helpful in my specific scenario and I’m not willing to give up on it just yet.
The method looks like this:
public setContent = (newDoc: IDocsEntry) => {
console.log('Updating with', newDoc);
this.setState(() => ({doc: newDoc}), () => console.log(this.state.doc));
};
and I’ve also tried
public setContent = (newDoc: IDocsEntry) => {
console.log('Updating with', doc);
this.setState(() => ({newDoc: {id: doc.id, path: doc.path, title: doc.title, content: doc.content}}), () => console.log(this.state.doc));
};
The issue is as following:
- on initially setting this state using this method, everything works as expected
- when I then close this component (it lives in a modal), do something else and then try to update the state using this method, it simply does not update
In my console logs, I can see that the method is called with the new and correct object, but the callback of setState
simply shows the old state from the first invocation of this function.
Edit: file / class structure
interface Props {
onClose: () => void
}
interface State {
open: boolean,
isEditor: boolean,
doc: IDocsEntry
}
class ModalDocsFile extends React.Component<Props, State> {
docPreEditing: IDocEntry | null = null;
constructor(props: Props) {
super(props);
this.state = {
open: false,
isEditor: false,
doc: {
id: -1,
path: '',
title: '',
content: ''
}
};
}
public show = () => {
this.setState(() => ({open: true}));
};
public setEditor = (edit: boolean, skipSave?: boolean) => {
this.setState(() => ({isEditor: edit}));
if (edit) this.docPreEditing = this.state.doc;
if (edit === false && !skipSave) this.save(); // editor is closed -> save
if (edit === false && skipSave) this.setState(state => ({doc: this.docPreEditing || state.doc})); // editor is closed without saving -> restore
};
public setContent = (newDoc: IDocsEntry) => {
console.log('Updating with', newDoc);
this.setState(state => ({...state, doc: newDoc}), () => console.log(this.state.doc));
};
private save = () => {
if (this.state.doc.title === '') return;
if (this.state.doc.id === -1) {
// is new
console.log('Saving new', this.state.doc);
fetchPOST({
url: 'https://api.crowbait.de/docs',
auth: true,
body: JSON.stringify(this.state.doc)
}).then(res => this.setState(state => ({doc: {...state.doc, id: parseInt(res as string)}})));
} else {
// update
console.log('Updating', this.state.doc);
fetchPOST({
url: `https://api.crowbait.de/docs/${this.state.doc.id}`,
auth: true,
body: JSON.stringify(this.state.doc)
});
}
};
render() {
// is viewer
if (!this.state.isEditor) {
return (
<BasePageModal open={this.state.open} onClose={() => {
this.setState(() => ({open: false}));
this.props.onClose();
}} height="80vh">
<DocViewer doc={this.state.doc} onEdit={() => this.setEditor(true)} />
</BasePageModal>
);
}
// is editor
return (
<BasePageModal open={this.state.open} onClose={() => {
if (this.state.isEditor) this.save();
this.setState(() => ({open: false}));
setTimeout(() => this.props.onClose(), 1000);
}} height="80vh">
<Grid container spacing={2} style={{height: '100%'}}>
<Grid xs={6} style={{paddingRight: 32, height: '100%'}}>
<TextField id="textfield-title" label="Title" value={this.state.doc.title}
fullWidth size="small" style={{marginTop: 16, marginRight: 16}}
onChange={e => this.setState(state => ({
doc: {
...state.doc,
title: e.target.value
}
}))}
error={!this.state.doc.title} />
<TextField id="textfield-path" label="Path" value={this.state.doc.path}
fullWidth size="small" style={{marginTop: 8, marginRight: 16}}
onChange={e => this.setState(state => ({
doc: {
...state.doc,
path: e.target.value
}
}))} />
<TextField id="textfield-content" label="Content" value={this.state.doc.content}
fullWidth size="small" multiline
style={{marginTop: 16, marginRight: 16, height: '80%'}}
inputProps={{style: {height: '100%', overflow: 'auto', fontFamily: 'monospace'}}}
InputProps={{style: {height: '100%'}}}
onChange={e => this.setState(state => ({
doc: {
...state.doc,
content: e.target.value.replaceAll('rn', 'n')
}
}))} />
</Grid>
<Grid xs={6} style={{height: '100%'}}>
<DocViewer doc={this.state.doc} isEditor onSave={() => this.setEditor(false)}
onRevert={() => this.setEditor(false, true)} />
</Grid>
</Grid>
</BasePageModal>
);
}
}
export default ModalDocsFile;
3
Answers
The problem was, as it is so often, human error.
Another contributor to the project made changes, ignoring just enough of the project's contributing, code-review and linting guidelines to make this particular change somewhat unnoticed.
Another component calling this
setContent
-method also called another method in another component, which (via several intermediate steps and around even more corners) undid everything thatsetContent
did.So this was entirely our fault and not at all some issue in language mastery or React, but antipatterns and failed control mechanisms.
What does your constructor looks like, maybe you forgot to bind setContent
or if your param name is newDoc: