Building a Rich Text Editor with React and Draft.js, Part 3: Persisting Rich Text Data to Server

Siobhan Mahoney
5 min readJul 9, 2018

--

This is the 3rd installment of a multi-part series about building a Rich Text Editor with React and Draft.js. For an introduction to the Draft.js framework with setup instructions, checkout Building a Rich Text Editor with Draft.js, Part 1: Basic Set Up. Links to additional posts in this series can be found below. The full codebase for my ongoing Draft.js project is available on GitHub. Feel free to checkout the deployed demo here.

In this post, I will review how to add Redux to your React app to manage Draft.js editor and state and how to persist Draft.js editor content to a Rails server.

Overview

In order to persist the content from the Draft.js editor to a server, it will be need to be converted from its native immutable data structure to a raw Javascript object. Thankfully, Draft.js offers straightforward utility functions to serialize and unserialize content:

  • convertFromRaw: converts raw state into a ContentState when restoring contents within a Draft.js editor
  • convertToRaw: converts a ContentState object into a raw Javascript object when saving an EditorState for storage, conversion to other formats, or other usage within an application

Let’s review how to implement these utility functions in a React/Redux application.

Rails Setup

For this demonstration, we’ll work with a simple schema with just one model, Note, which will have fields title and content. If you would like to follow along, feel free to clone my Rails server from GitHub.

React & Draft.js Setup

For reference, I’ll be integrating these features into the React basic text editor I built for the demo discussed in the first post in this series, Building a Rich Text Editor with Draft.js, Part 1: Basic Set Up, the code for which is available on CodeSandbox.

Draft.js can be installed with:

yarn add draft-js

Redux Setup

You’ll want to install the following dependencies:

Redux

yarn add redux

React-Redux

yarn add react-redux

Redux-Thunk

yarn add redux-thunk

Once the Redux and Redux-related dependencies have been installed, we’ll set up our Redux state, reducers, and a few actions:

  • ./src/actions/index.js:
export const LOAD_NOTE = "LOAD_NOTE";
export const UPDATE_NOTE = "UPDATE_NOTE";
export const CREATE_NOTE = "CREATE_NOTE";
export function loadNote() {
return dispatch => {
fetch("http://localhost:3000/api/v1/notes")
.then(response => response.json())
.then(json => dispatch({ type: LOAD_NOTE, payload: json })
)}
}
export function createNote(noteContent) {
return dispatch => {
fetch("http://localhost:3000/api/v1/notes", {
method: "post",
headers: { "Content-Type": "application/json", "Accepts": "application/json" },
body: JSON.stringify({ content: noteContent })
})
.then(response => response.json())
.then(json => {
dispatch({ type: CREATE_NOTE, newNote: json })
})
}
}
export function updateNote(note_id, note_content) {
return dispatch => {
fetch(`http://localhost:3000/api/v1/notes/${note_id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json", Accepts: "application/json" },
body: JSON.stringify({ content: note_content })
})
.then(response => response.json())
.then(json =>
dispatch({ type: UPDATE_NOTE, updated_note: json })
);
};
}
  • ./src/reducers/index.js:
import { LOAD_NOTE, UPDATE_NOTE, CREATE_NOTE } from "../actions";const note = (state = { displayedNote: null }, action) => {
switch (action.type) {
case LOAD_NOTE:
state = Object.assign({}, state, {
displayedNote: action.payload[0] || null
});
return state;
case CREATE_NOTE:
let newNote = action.newNote;
state = Object.assign({}, state, {
displayedNote: newNote
});
return state;
case UPDATE_NOTE:
state = Object.assign({}, state, {
displayedNote: action.updated_note
});
return state;
default:
return state;
}
};
const rootReducer = note;export default rootReducer;
  • ./src/index.js:
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import registerServiceWorker from "./registerServiceWorker";
import { createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { Provider } from "react-redux";
import rootReducer from "./reducers";
import * as Actions from "./actions";
function configureStore() {
return createStore(rootReducer, applyMiddleware(thunk));
}
const store = configureStore();store.subscribe(() => {
store.getState();
});
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
registerServiceWorker();

Saving EditorState to Database with Redux

Now that we have our database schema and Redux pieces in place, let’s take a look at how to engineer our React component to handle the EditorState properly:

class PageContainer extends React.Component {{/* ... */}

componentDidMount() {
if (this.props.note === null) {
this.setState({
displayedNote: "new",
editorState: EditorState.createEmpty()
})
} else {
this.setState({
displayedNote: this.props.note.id,
editorState: EditorState.createWithContent(convertFromRaw(JSON.parse(this.props.note.content)))
})
}
}

componentDidUpdate(prevProps, prevState) {
if (prevProps.note == null && !!this.props.note) {
this.props.loadNote
this.setState({
displayedNote: this.props.note.id,
editorState: EditorState.createWithContent(convertFromRaw(JSON.parse(this.props.note.content)))
})
}
}

onChange = (editorState) => {
this.setState({ editorState: editorState });
};

submitEditor = () => {
let contentState = this.state.editorState.getCurrentContent()
if (this.state.displayedNote == "new") {
let note = {content: convertToRaw(contentState)}
note["content"] = JSON.stringify(note.content)
this.props.createNote(note.content)
} else {
let note = {content: convertToRaw(contentState)}
note["content"] = JSON.stringify(note.content)
this.props.updateNote(this.state.displayedNote, note.content)
}
}

{/* ... */}

}

Here’s what’s happening in the snippet:

  • componentDidMount: The editor will load an empty EditorState if a note does not exist in our database. If a note does exist in our database, we use convertFromRow and JSON.parse to transform the data from our database into an immutable object so that it can be load as the contents of the note into the editor
  • submitEditor: When a user creates or saves changes within the editor, the EditorState is converted to a raw Javascript object and then a string that can be stored in our database using convertToRaw and JSON.stringify().

And that’s it! Easy peasy.

Curious about what else you can do with Draft.js? Checkout the other posts in this series!

--

--