Search code examples
javascriptreactjsreduxlocal-storagedraftjs

How to read DraftJS state from localStorage?


I have an issue with reading Draft.js raw content from localStorage.

I would like to use previously stored rawContent as my initialState for the reducer.

I think convertFromRaw function is my problem. I does crush silently (does not log to the console) and I don't know how to troubleshoot this.

I'm persisting Draft.js raw content in localStorage:

const contentState = editorState.getCurrentContent();
const rawContent = convertToRaw(contentState);
window.localStorage.setItem(
  "rawContent",
  JSON.stringify(convertToRaw(rawContent))
);

And then wen I'm trying to restore it I'm facing my problem. I think convertFromRaw.

const rawContent = window.localStorage.getItem("rawContent");

if (rawContent) {
  this.state.editorState = EditorState.createWithContent(
    convertFromRaw(JSON.parse(rawContent))
  );
} else {
  this.state.editorState = EditorState.createEmpty();
}

My temporary solution to this was to use a third party converter. In my case I used stateToMarkdown / stateFromMarkdown from draft-js-export-markdown / draft-js-import-markdown

My package.json

{
  "name": "react-boilerplate",
  "version": "3.7.0",
  "description": "A highly scalable, offline-first foundation with the best DX and a focus on performance and best practices",
  "repository": {
    "type": "git",
    "url": "git://github.com/react-boilerplate/react-boilerplate.git"
  },
  "engines": {
    "npm": ">=5",
    "node": ">=8.10.0"
  },
  "author": "Max Stoiber",
  "license": "MIT",
  "scripts": {
    "analyze:clean": "rimraf stats.json",
    "preanalyze": "npm run analyze:clean",
    "analyze": "node ./internals/scripts/analyze.js",
    "extract-intl": "node ./internals/scripts/extract-intl.js",
    "npmcheckversion": "node ./internals/scripts/npmcheckversion.js",
    "preinstall": "npm run npmcheckversion",
    "prebuild": "npm run build:clean",
    "build": "cross-env NODE_ENV=production webpack --config internals/webpack/webpack.prod.babel.js --color -p --progress --hide-modules --display-optimization-bailout",
    "build:clean": "rimraf ./build",
    "start": "cross-env NODE_ENV=development node server",
    "start:tunnel": "cross-env NODE_ENV=development ENABLE_TUNNEL=true node server",
    "start:production": "npm run test && npm run build && npm run start:prod",
    "start:prod": "cross-env NODE_ENV=production node server",
    "presetup": "npm i chalk shelljs",
    "setup": "node ./internals/scripts/setup.js",
    "clean": "shjs ./internals/scripts/clean.js",
    "clean:all": "npm run analyze:clean && npm run test:clean && npm run build:clean",
    "generate": "plop --plopfile internals/generators/index.js",
    "lint": "npm run lint:js",
    "lint:css": "stylelint './app/**/*.js'",
    "lint:eslint": "eslint --ignore-path .gitignore --ignore-pattern internals/scripts",
    "lint:eslint:fix": "eslint --ignore-path .gitignore --ignore-pattern internals/scripts --fix",
    "lint:js": "npm run lint:eslint -- . ",
    "lint:staged": "lint-staged",
    "pretest": "npm run test:clean && npm run lint",
    "test:clean": "rimraf ./coverage",
    "test": "cross-env NODE_ENV=test jest --coverage",
    "test:watch": "cross-env NODE_ENV=test jest --watchAll",
    "coveralls": "cat ./coverage/lcov.info | coveralls",
    "prettify": "prettier --write"
  },
  "jest": {
    "collectCoverageFrom": [
      "app/**/*.{js,jsx}",
      "!app/**/*.test.{js,jsx}",
      "!app/app.js",
      "!app/global-styles.js",
      "!app/*/*/Loadable.{js,jsx}"
    ],
    "coverageThreshold": {
      "global": {
        "statements": 50,
        "branches": 40,
        "functions": 50,
        "lines": 50
      }
    },
    "moduleDirectories": [
      "node_modules",
      "app"
    ],
    "moduleNameMapper": {
      ".*\\.(css|less|styl|scss|sass)$": "<rootDir>/internals/mocks/cssModule.js",
      ".*\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/internals/mocks/image.js"
    },
    "setupTestFrameworkScriptFile": "<rootDir>/internals/testing/test-bundler.js",
    "testRegex": "tests/.*\\.test\\.js$"
  },
  "lint-staged": {
    "*.js": [
      "npm run lint:eslint:fix",
      "git add --force"
    ],
    "*.json": [
      "prettier --write",
      "git add --force"
    ]
  },
  "pre-commit": "lint:staged",
  "resolutions": {
    "babel-core": "7.0.0-bridge.0"
  },
  "dependencies": {
    "@babel/polyfill": "7.0.0",
    "antd": "^3.13.1",
    "axios": "^0.18.0",
    "chalk": "2.4.1",
    "compression": "1.7.3",
    "connected-react-router": "4.5.0",
    "cross-env": "5.2.0",
    "draft-js": "^0.10.5",
    "draft-js-export-html": "^1.3.3",
    "draft-js-export-markdown": "^1.3.3",
    "draft-js-import-html": "^1.3.3",
    "draft-js-import-markdown": "^1.3.3",
    "express": "4.16.4",
    "fontfaceobserver": "2.0.13",
    "history": "4.7.2",
    "hoist-non-react-statics": "3.0.1",
    "immutable": "3.8.2",
    "intl": "1.2.5",
    "invariant": "2.2.4",
    "ip": "1.1.5",
    "loadable-components": "2.2.3",
    "lodash": "4.17.11",
    "minimist": "1.2.0",
    "prop-types": "15.6.2",
    "react": "16.6.0",
    "react-dom": "16.6.0",
    "react-draft-wysiwyg": "^1.13.2",
    "react-flexview": "^4.0.3",
    "react-helmet": "5.2.0",
    "react-intl": "2.7.2",
    "react-logger-lib": "^1.0.5",
    "react-rbac-guard": "0.0.3",
    "react-redux": "5.0.7",
    "react-router-dom": "4.3.1",
    "redux": "4.0.1",
    "redux-immutable": "4.0.0",
    "redux-saga": "0.16.2",
    "reselect": "4.0.0",
    "sanitize.css": "4.1.0",
    "styled-components": "4.0.2",
    "warning": "4.0.2"
  },
  "devDependencies": {
    "@babel/cli": "7.1.2",
    "@babel/core": "7.1.2",
    "@babel/plugin-proposal-class-properties": "7.1.0",
    "@babel/plugin-syntax-dynamic-import": "7.0.0",
    "@babel/plugin-transform-modules-commonjs": "7.1.0",
    "@babel/plugin-transform-react-constant-elements": "7.0.0",
    "@babel/plugin-transform-react-inline-elements": "7.0.0",
    "@babel/preset-env": "7.1.0",
    "@babel/preset-react": "7.0.0",
    "@babel/register": "7.0.0",
    "add-asset-html-webpack-plugin": "3.1.1",
    "babel-core": "7.0.0-bridge.0",
    "babel-eslint": "10.0.1",
    "babel-loader": "8.0.4",
    "babel-plugin-dynamic-import-node": "2.2.0",
    "babel-plugin-import": "^1.11.0",
    "babel-plugin-lodash": "3.3.4",
    "babel-plugin-react-intl": "3.0.1",
    "babel-plugin-react-transform": "3.0.0",
    "babel-plugin-styled-components": "1.8.0",
    "babel-plugin-transform-react-remove-prop-types": "0.4.19",
    "circular-dependency-plugin": "5.0.2",
    "compare-versions": "3.4.0",
    "compression-webpack-plugin": "2.0.0",
    "coveralls": "3.0.2",
    "css-loader": "1.0.0",
    "enzyme": "3.7.0",
    "enzyme-adapter-react-16": "1.6.0",
    "enzyme-to-json": "3.3.4",
    "eslint": "5.7.0",
    "eslint-config-airbnb": "17.1.0",
    "eslint-config-airbnb-base": "13.1.0",
    "eslint-config-prettier": "3.1.0",
    "eslint-import-resolver-webpack": "0.10.1",
    "eslint-plugin-import": "2.14.0",
    "eslint-plugin-jsx-a11y": "6.1.2",
    "eslint-plugin-prettier": "3.0.0",
    "eslint-plugin-react": "7.11.1",
    "eslint-plugin-redux-saga": "0.9.0",
    "file-loader": "2.0.0",
    "html-loader": "0.5.5",
    "html-webpack-plugin": "3.2.0",
    "image-webpack-loader": "^4.6.0",
    "imports-loader": "0.8.0",
    "jest-cli": "^24.5.0",
    "jest-styled-components": "6.2.2",
    "less": "^3.9.0",
    "less-loader": "^4.1.0",
    "lint-staged": "7.3.0",
    "ngrok": "3.1.0",
    "node-plop": "0.16.0",
    "null-loader": "0.1.1",
    "offline-plugin": "5.0.5",
    "plop": "2.1.0",
    "pre-commit": "1.2.2",
    "prettier": "1.14.3",
    "react-app-polyfill": "0.1.3",
    "react-test-renderer": "16.6.0",
    "rimraf": "2.6.2",
    "shelljs": "0.8.2",
    "style-loader": "0.23.1",
    "stylelint": "^9.10.1",
    "stylelint-config-recommended": "2.1.0",
    "stylelint-config-styled-components": "0.1.1",
    "stylelint-processor-styled-components": "1.5.0",
    "svg-url-loader": "2.3.2",
    "terser-webpack-plugin": "1.1.0",
    "url-loader": "1.1.2",
    "webpack": "4.23.1",
    "webpack-cli": "3.1.2",
    "webpack-dev-middleware": "3.4.0",
    "webpack-hot-middleware": "2.24.3 ",
    "webpack-pwa-manifest": "3.7.1",
    "whatwg-fetch": "3.0.0"
  }
}


Solution

  • Your code looks correct generally. But as I can see you are using convertToRaw twice.

    const contentState = editorState.getCurrentContent();
    const rawContent = convertToRaw(contentState); // <== convert to raw first time
    window.localStorage.setItem(
      "rawContent",
      JSON.stringify(convertToRaw(rawContent)) // <== convert to raw again
    );
    

    Try to rewrite JSON.stringify(convertToRaw(rawContent)) to JSON.stringify(rawContent), I think it should fix your problem.

    If that doesn't help you can check this jsFiddle which shows the common pattern how Draft.js works with local storage - https://jsfiddle.net/x2gsp6ju/4/

    In this demo, you can see simple editor component, when you click on SAVE RAW CONTENT TO LOCAL STORAGE, we save current editor content as a string to local storage. We use convertToRaw and JSON.stringify for it:

     saveRaw = () => {
      var contentRaw = convertToRaw(this.state.editorState.getCurrentContent());
    
      localStorage.setItem('draftRaw', JSON.stringify(contentRaw));
    }
    

    If after that you reload the page, your editor will be initialized with the content and styles what you save. Becouse of in constructor we read the appropriate local storage property, and with JSON.parse, convertFromRaw and createWithContent methods initialize editor with the previously stored content.

    constructor(props) {
      super(props);
    
      let initialEditorState = null;
      const storeRaw = localStorage.getItem('draftRaw');
    
      if (storeRaw) {
        const rawContentFromStore = convertFromRaw(JSON.parse(storeRaw));
        initialEditorState = EditorState.createWithContent(rawContentFromStore);
      } else {
        initialEditorState = EditorState.createEmpty();
      }
    
      this.state = {
        editorState: initialEditorState
      };
    }