JSONCrack Codebase Analysis — Part 4.2.1 — Editor — JsonEditor Component
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
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