JSONCrack Codebase Analysis — Part 4.2.1 — Editor — JsonEditor Component

·

7 min read

jsoncrack is a popular opensource tool used to visualise json into a mindmap. It is built using Next.js.

We, at TThroo, love open source and perform codebase analysis on popular repositories, document, and provide a detailed explanation of the codebase. This enables OSS enthusiasts to learn and contribute to open source projects, and we also apply these learnings in our projects.

Part 4.1 talks about Panes component that has this component JsonEditor

Do what does JsonEditor do? the below image shows what it is responsible for.

JsonEditor.tsx has about 23 lines at the time of writing this article.

import React from "react";
import styled from "styled-components";
import { MonacoEditor } from "src/components/MonacoEditor";

// import { PromptInput } from "src/components/PromptInput";
const StyledEditorWrapper = styled.div`
  display: flex;
  flex-direction: column;
  height: 100%;
  user-select: none;
`;
export const JsonEditor: React.FC = () => {
  return (
    <StyledEditorWrapper>
      {/* <PromptInput /> */}
      <MonacoEditor />
    </StyledEditorWrapper>
  );
};
export default JsonEditor;

Aye, commented code? anyways, You see this tool uses popular MonacoEditor, a browser based open source code editor from Microsoft.

MonacoEditor component

Now notice how we are now moving away from JsonEditor component to MonacoEditor. The author could have just put all the code related to MonacoEditor in this JsonEditor component, but no, it has been moved to components folder. One possible answer could be jsoncrack author must have decided to include PromtInput as part of JsonEditor, but it was commented. Purely, to add more features along side the monaco editor, I think MonacoEditor is made a separate new component.

Let’s now understand what MonacoEditor does.

Top down approach, this component returns the following:

return (
    <StyledWrapper>
      <Editor
        height="100%"
        language={fileType}
        theme={theme}
        value={contents}
        options={editorOptions}
        onValidate={errors => setError(errors[0]?.message)}
        onChange={contents => setContents({ contents, skipUpdate: true })}
        loading={<Loading message="Loading Monaco Editor..." loading />}
      />
    </StyledWrapper>
  );

Let’s go through each prop in detail.

language={fileType}

Where is fileType coming from? Checking the MonacoEditor.tsx code, we see

const fileType = useFile(state => state.format);

useFile? no worries, it is zustand store.

This useFile has initial state as follows

const initialStates = {
  fileData: null as File | null,
  format: FileFormat.JSON,
  contents: defaultJson,
  error: null as any,
  hasChanges: false,
  jsonSchema: null as object | null,
};

Always look for what you are interested in, do not let the extra props, additions functions, fancy typescript type scare you away. It helps you stay focused. so what we are looking for is FileFormat.JSON

FileFormat? it is simple enum file. This goes to say that jsoncrack author follows standard coding practices in not using magical strings or numbers instead relies on enums.

Notice how we approached in understanding fileType prop, it was unknown but we carefully looked at imports and specifically looked for it without letting the code inside useFile scare us away :)

theme={theme}

Let’s take the same approach as we did for fileType.

const theme = useConfig(state => (state.darkmodeEnabled ? "vs-dark" : "light"));

you will find the above snippet here

What are the unknowns here? yes, useConfig

useConfig

We are specifically looking for an initialState with the prop darkModeEnabled

const initialStates = {
  darkmodeEnabled: false,
  collapseButtonVisible: true,
  childrenCountVisible: true,
  imagePreviewEnabled: true,
  liveTransformEnabled: true,
  gesturesEnabled: false,
  rulersEnabled: true,
  viewMode: ViewMode.Graph,
};

You will find the above code snippet here

So by default, darkmodeEnabled is set to false as part of initial state.

value={contents}

Let’s take the same approach as we did for theme.

const contents = useFile(state => state.contents);

You will find the above code snippet here.

back to useFile, except this time we are interested in a different prop named contents.

useFile initialState

const initialStates = {
  fileData: null as File | null,
  format: FileFormat.JSON,
  contents: defaultJson,
  error: null as any,
  hasChanges: false,
  jsonSchema: null as object | null,
};

Pick what you are interested in, yes, contents. what is a defaulJson ? the answer is in import

This is how you find answers, You have to be patient. There is a back and forth jumping between different files, that is because author has defined such a pattern.

Meaning, all the zustand stores and inside a folder called store inside src. Large project complexity can be maintained well when you have well defined design pattern. You should keep it simple and know where to look for. Back to the code analysis now.

defaultJson is used to render some placeholder json. Notice how this is inside src/constants/data.ts. Again, I am praising the way it is organised.

options={editorOptions}

editorOptions are to provide your options for your monaco editor.

onValidate={errors => setError(errors[0]?.message)}

onValidate used to set errors in the useFile store

You can read more about it here

onChange={contents => setContents({ contents, skipUpdate: true })}

setContents has few side effects.

setContents: async ({ contents, hasChanges = true, skipUpdate = false }) => {
    try {
      set({ ...(contents && { contents }), error: null, hasChanges });
      const isFetchURL = window.location.href.includes("?");
      const json = await contentToJson(get().contents, get().format);
      if (!useConfig.getState().liveTransformEnabled && skipUpdate) return;
      if (get().hasChanges && contents && contents.length < 80_000 && !isIframe() && !isFetchURL) {
        sessionStorage.setItem("content", contents);
        sessionStorage.setItem("format", get().format);
        set({ hasChanges: true });
      }
      debouncedUpdateJson(json);
    } catch (error: any) {
      if (error?.mark?.snippet) return set({ error: error.mark.snippet });
      if (error?.message) set({ error: error.message });
      useJson.setState({ loading: false });
      useGraph.setState({ loading: false });
    }
  },

The prop setContents({ contents, skipUpdate: true }) only sets skipUpdate and contents, setContents: async ({ contents, hasChanges = true, skipUpdate = false }) hasChanges is defaulted to true.

set({ ...(contents && { contents }), error: null, hasChanges }); updates the contents in the useFile store.

const isFetchURL = window.location.href.includes("?"); apparently, jsoncrack does not auto save your edits. You have to click on save to cloud in the BottomBar to save your json and then you will see your url changing to something like https://jsoncrack.com/editor?json=0d15b1a928fdbd3c3283d1e6

I have done to understand what isFetchURL does. now we know why.

const json = await contentToJson(get().contents, get().format);

contentToJson function name indicates that it takes monacoeditor content and converts it to json. Let's see what code is.

This contentToJson is coming from https://github.com/AykutSarac/jsoncrack.com/blob/main/src/lib/utils/json/jsonAdapter.ts#L34.

Notice how we are using a file from src/lib/utils/json/jsonAdapter. Author could have just put this in MonacoEditor, but when you are interested and making efforts to organise your code to use lib/utils/json/jsonAdapter to put your helpers in a different file is how you level up your coding game. Nobody will teach you this. Seasoned devs always try to put helper functions where they belong.

const contentToJson = async (value: string, format = FileFormat.JSON): Promise<object> => {
  try {
    let json: object = {};
    if (format === FileFormat.JSON) json = parse(value);
    if (format === FileFormat.YAML) json = load(value) as object;
    if (format === FileFormat.XML) json = jxon.stringToJs(value);
    if (format === FileFormat.TOML) json = toml.parse(value);
    if (format === FileFormat.CSV) json = await csv2json(value);
    if (format === FileFormat.XML && keyExists(json, "parsererror")) throw Error("Unknown error!");
    if (!json) throw Error("Invalid JSON!");
    return Promise.resolve(json);
  } catch (error: any) {
    throw error;
  }
};

Okay, it is simple parse on contents from monaco editor since contents are json.

if (!useConfig.getState().liveTransformEnabled && skipUpdate) return;

This is directly related to the BottomBar as shown in image, notice how zustand makes it easy to establish state communication between Editor component and Bottombar

if (get().hasChanges && contents && contents.length < 80_000 && !isIframe() && !isFetchURL) {
  sessionStorage.setItem("content", contents);
  sessionStorage.setItem("format", get().format);
  set({ hasChanges: true });
}

So far, hasChanges is true, contents has got some data, isIframe is false and isFetchURL is false. This if block gets executed and saves the data into sessionStorage.

Finally

debouncedUpdateJson(json);

debouncedupdateJson has the following code:

const debouncedUpdateJson = debounce((value: unknown) => {
  useGraph.getState().setLoading(true);
  useJson.getState().setJson(JSON.stringify(value, null, 2));
}, 800);

It is a super common pattern to use debounce when you are dealing with user input and to save the data. Read more about it here

useJson.getState().setJson deserves a new article 4.2.1 because of the following code snippets and the side effects involved with useGraph

const useJson = create<JsonStates & JsonActions>()((set, get) => ({
  ...initialStates,
  getJson: () => get().json,
  setJson: json => {
    set({ json, loading: false });
    useGraph.getState().setGraph(json);
  },
  clear: () => {
    set({ json: "", loading: false });
    useGraph.getState().clearGraph();
  },
}));

The above snippet is from here

Once we understand, useJson and useGraph, it lays a foundation to further explore different use cases involved with yaml, xml, csv formats as well.

Conclusion:

We looked at how you need to be specific about what you are looking for, especially when you have lot of unknowns, do not let this scare you away, I keep saying this, you have to comfortable with not knowing enough YET. We looked at props used in MonacoEditor, a lot of the props were straight forward. Because of the complexity and side effects involved in debouncedJson's useGraph and useJson, we decided to make a new article 4.2.1.1 talking in depth about useJson.getState().setJson and the side effect involving setGraph. Thank you for reading till the end. If you have any questions or need help with a project or looking for an IT partner for your business, feel free to reach out to us at ram@tthroo.com