Building a Rich Text Editor with React and Draft.js, Part 3: Persisting Rich Text Data to Server
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 aContentState
when restoring contents within a Draft.js editorconvertToRaw
: converts aContentState
object into a raw Javascript object when saving anEditorState
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 emptyEditorState
if a note does not exist in our database. If a note does exist in our database, we useconvertFromRow
andJSON.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 editorsubmitEditor
: When a user creates or saves changes within the editor, theEditorState
is converted to a raw Javascript object and then a string that can be stored in our database usingconvertToRaw
andJSON.stringify()
.
And that’s it! Easy peasy.