CM6 with Redux

Hi team, does this library work in a React, Redux stack? Is this even a supported feature? If not, is there any work towards making it compatible with React/Redux state management?

If that involves serializing the editor state to JSON on every update, then not really. But the state/transaction model of the library is inspired by Redux and should work well with that kind of architecture. There’s no React glue in the core library though, so you’ll have to do a bit of wrapping.

@marijn When you say wrapping, do you mean putting into a react component? For example, we have existing react ts code that looks a little something like this:

import {Button, Input} from 'antd';
import {
  LoadingOutlined,
  UploadOutlined,
  RollbackOutlined,
  DeleteOutlined,
  CheckCircleOutlined,
} from '@ant-design/icons';

import {EditorState, EditorView, basicSetup} from "@codemirror/basic-setup";
import {PostgreSQL} from "@codemirror/lang-sql";

// import {CompletionContext, CompletionResult, CompletionSource} from '@codemirror/next/autocomplete';
// import {schemaCompletion, PostgreSQL, MySQL, SQLConfig} from '@codemirror/next/lang-sql';

import * as React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators, Dispatch} from 'redux';

import {getRules} from '../../reducers/rules';
import {updateRuleBody, saveRule, deleteRule} from '../../actions/rules';

import {State, SnowAlertRulesState} from '../../reducers/types';
import sqlFormatter from 'sql-formatter';

import './RawEditor.css';

interface OwnProps {
  currentRuleView: string | null;
}

interface DispatchProps {
  updateRuleBody: typeof updateRuleBody;
  saveRule: typeof saveRule;
  deleteRule: typeof deleteRule;
}

interface StateProps {
  rules: SnowAlertRulesState;
}

type RawEditorProps = OwnProps & DispatchProps & StateProps;

class RawEditor extends React.PureComponent<RawEditorProps> {
  render(): JSX.Element {
    const {currentRuleView, deleteRule, saveRule, updateRuleBody} = this.props;
    const {queries, suppressions} = this.props.rules;
    const rules = [...queries, ...suppressions];
    const rule = rules.find((r) => r.viewName === currentRuleView);

    return (
      <div>
        <Input.TextArea
          disabled={!rule || rule.isSaving}
          value={rule ? rule.raw.body : ''}
          spellCheck={false}
          autoSize={{minRows: 30}}
          onChange={(e) => rule && updateRuleBody(rule.viewName, e.target.value)}
        />
        <div className="app"></div>

        <Button
          type="primary"
          disabled={!rule || rule.isSaving || (rule.isSaved && !rule.isEdited)}
          onClick={() => rule && saveRule(rule)}
        >
          {rule && rule.isSaving ? <LoadingOutlined /> : <UploadOutlined />} Apply
        </Button>
        <Button
          type="default"
          disabled={!rule || rule.isSaving || (rule.isSaved && !rule.isEdited)}
          onClick={() => rule && updateRuleBody(rule.viewName, rule.raw.savedBody)}
        >
          <RollbackOutlined /> Revert
        </Button>
        <Button type="default" disabled={!rule || rule.isSaving} onClick={() => rule && deleteRule(rule.raw)}>
          <DeleteOutlined /> Delete
        </Button>
        <Button
          type="default"
          disabled={!rule || rule.isSaving}
          onClick={() => rule && updateRuleBody(rule.viewName, sqlFormatter.format(rule.raw.body))}
        >
          <CheckCircleOutlined /> Format
        </Button>
      </div>
    );
  }
}

const mapDispatchToProps = (dispatch: Dispatch) => {
  return bindActionCreators(
    {
      updateRuleBody,
      saveRule,
      deleteRule,
    },
    dispatch,
  );
};

const mapStateToProps = (state: State) => {
  return {
    rules: getRules(state),
  };
};

export default connect(mapStateToProps, mapDispatchToProps)(RawEditor);

We’re trying to replace the Input.TextArea above with this editor below:

    let startState = EditorState.create({
        doc: 'Hello World',
        extensions: [basicSetup, PostgreSQL]
    })

    let view = (window).view = new EditorView({state: startState})
    const editorDiv = document.querySelector('#editor');
    editorDiv.innerHTML = '';
    editorDiv.appendChild(view.dom)

Do you know if this is possible? As you can see we’re trying to load the code to the editor from a data source like so:

<Input.TextArea
    disabled={!rule || rule.isSaving}
    value={rule ? rule.raw.body : ''}
    spellCheck={false}
    autoSize={{minRows: 30}}
    onChange={(e) => rule && updateRuleBody(rule.viewName, e.target.value)}
/>

and were wondering how the editor state can be set using the above code. Any advise from you will help us immensely. :pray:

I found this git gist on a possible wrapper. Is this what you were referring to @marijn?

I don’t use react myself, so I don’t have a terribly informed perspective on this, but I imagine a Redux-like architecture would keep the editor state in some larger state store, use dispatch to create actions that update that, and have the state from the store flow into the editor again.

But, now that I think about it, the way view.update requires the transactions to efficiently update does kind of get in the way there. So you’d probably want to add a kludge where you immediately update your view in dispatch and then asynchronously broadcast the updated state, and make sure to only call setState when a different state comes in through a prop.

I’m currently tackling the same issue. I saw some problems with the solution linked above. So, I came up with my own wrapper component (written in linted TypeScript):

Note: this is using defaultTabBinding. An option to disable it is currently missing. (See the following paragraph.)

What’s still missing is a way to pass options as props. But i think it’s fine to create one prop to pass all options in one go. This would also handle events/listeners if I understand the API correctly. The onContentChange prop is for convenience because it’s so common.

What’s also missing is a way to do updates to settings and the EditorState. But all you need to do that is have access to editorViewRef (EditorView), right?

So, all in all it doesn’t sound too difficult to add those features.

@marijn you’re basically correct about how this would work (it’s just a little hard to tell if/when you’re talking about Redux or CodeMirror states/dispatch :sweat_smile: They exist in both). And yes, I’ve noticed I needed to check if the document actually changed, otherwise CodeMirror would reset the cursor position.

Comparing the incoming data with the current document kind of feels like wasted performance, but I couldn’t see any issues while spamming an CM instance with a lot of text (I’ve checked up to 5 MB of text, because then the localStorage quote exceeded because this is where I persist the document :D). Maybe it’s a good idea to throttle the dispatch calls?

The library depends on dispatch (CodeMirror’s dispatch) to run synchronously, so that wouldn’t work. If you’re doing something that’s generating a lot of state transitions in sequence, I find it’s usually possible to redesign it to put more information into the initial transaction and set up your state field reducers to apply all the needed changes from that.

Oh, wait… I’ve talked nonsense. I meant throttling the update logic of the React Wrapper. This one gets called on every keystroke and whenever the document should be changed programmatically. This causes a lot of read operations on CodeMirror in quick succession.

By throttling this logic the comparison of the incoming document and the current document in CodeMirror happens only once a second or something like that. CodeMirror’s dispatch gets called directly afterwards if the documents are different - which should only happen on programmatic document changes.

At least I don’t see any problems with asynchronicity here. CodeMirror shouldn’t care if and when dispatch gets called.

No, indeed, that sounds fine.