skip to Main Content

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


  1. Chosen as BEST ANSWER

    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 that setContent did.

    So this was entirely our fault and not at all some issue in language mastery or React, but antipatterns and failed control mechanisms.


  2. What does your constructor looks like, maybe you forgot to bind setContent

      this.setContent = this.setContent.bind(this);
    
    Login or Signup to reply.
  3. public setContent = (newDoc: IDocsEntry) => {
        this.setState((prev) => { 
          //correct way to return and/or update state object
          return {...prev, doc: newDoc as IDocsEntry} 
        }
      };
    

    or if your param name is newDoc:

    public setContent = (newDoc: IDocsEntry) => {
        this.setState((prev) => {
          return {...prev, newDoc } 
        }
      };
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search