Skip to content

Commit 2b2c6fa

Browse files
authored
chore: improve state management and reduce re-renders (#103)
1 parent eeff72d commit 2b2c6fa

19 files changed

+231
-262
lines changed

src/components/Embedded.js

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Preview from './Preview';
77
import Query from './Query';
88
import Result from './Result';
99
import MarkupEditor from './MarkupEditor';
10-
import { PlaygroundProvider } from './Context';
10+
import usePlayground from '../hooks/usePlayground';
1111

1212
function onStateChange({ markup, query, result }) {
1313
state.save({ markup, query });
@@ -25,6 +25,11 @@ const SUPPORTED_PANES = {
2525

2626
// TODO: we should support readonly mode
2727
function Embedded() {
28+
const [{ markup, query, result }, dispatch] = usePlayground({
29+
onChange: onStateChange,
30+
...initialValues,
31+
});
32+
2833
const location = useLocation();
2934
const params = queryString.parse(location.search);
3035

@@ -53,26 +58,40 @@ function Embedded() {
5358
}, []);
5459

5560
return (
56-
<PlaygroundProvider onChange={onStateChange} initialValues={initialValues}>
57-
<div
58-
className={`h-full overflow-hidden grid grid-flow-col gap-4 p-4 bg-white shadow rounded ${columnClass}`}
59-
>
60-
{panes.map((area) => {
61-
switch (area) {
62-
case 'preview':
63-
return <Preview key={area} />;
64-
case 'markup':
65-
return <MarkupEditor key={area} />;
66-
case 'query':
67-
return <Query key={area} />;
68-
case 'result':
69-
return <Result key={area} />;
70-
default:
71-
return null;
72-
}
73-
})}
74-
</div>
75-
</PlaygroundProvider>
61+
<div
62+
className={`h-full overflow-hidden grid grid-flow-col gap-4 p-4 bg-white shadow rounded ${columnClass}`}
63+
>
64+
{panes.map((area) => {
65+
switch (area) {
66+
case 'preview':
67+
return (
68+
<Preview
69+
key={area}
70+
markup={markup}
71+
result={result}
72+
dispatch={dispatch}
73+
/>
74+
);
75+
case 'markup':
76+
return (
77+
<MarkupEditor key={area} markup={markup} dispatch={dispatch} />
78+
);
79+
case 'query':
80+
return (
81+
<Query
82+
key={area}
83+
query={query}
84+
result={result}
85+
dispatch={dispatch}
86+
/>
87+
);
88+
case 'result':
89+
return <Result key={area} result={result} dispatch={dispatch} />;
90+
default:
91+
return null;
92+
}
93+
})}
94+
</div>
7695
);
7796
}
7897

src/components/MarkupEditor.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import React, { useCallback } from 'react';
22
import Editor from './Editor';
3-
import { usePlayground } from './Context';
4-
5-
function MarkupEditor() {
6-
const { dispatch, state } = usePlayground();
73

4+
function MarkupEditor({ markup, dispatch }) {
85
const onLoad = useCallback(
96
(editor) => dispatch({ type: 'SET_MARKUP_EDITOR', editor }),
107
[dispatch],
@@ -20,7 +17,7 @@ function MarkupEditor() {
2017
<div className="markup-editor flex-auto relative overflow-hidden">
2118
<Editor
2219
mode="html"
23-
initialValue={state.markup}
20+
initialValue={markup}
2421
onLoad={onLoad}
2522
onChange={onChange}
2623
/>
@@ -29,4 +26,4 @@ function MarkupEditor() {
2926
);
3027
}
3128

32-
export default MarkupEditor;
29+
export default React.memo(MarkupEditor);

src/components/Playground.js

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,51 @@ import Preview from './Preview';
44
import MarkupEditor from './MarkupEditor';
55
import Result from './Result';
66
import Query from './Query';
7-
import { PlaygroundProvider } from './Context';
7+
import usePlayground from '../hooks/usePlayground';
88
import state from '../lib/state';
99

1010
function onStateChange({ markup, query, result }) {
1111
state.save({ markup, query });
1212
state.updateTitle(result?.expression?.expression);
1313
}
1414

15-
const initialValues = state.load();
15+
const initialValues = state.load() || {};
1616

1717
function Playground() {
18+
const [{ markup, query, result }, dispatch] = usePlayground({
19+
onChange: onStateChange,
20+
...initialValues,
21+
});
22+
1823
return (
19-
<PlaygroundProvider onChange={onStateChange} initialValues={initialValues}>
20-
<div className="flex flex-col h-auto md:h-full w-full">
21-
<div className="editor markup-editor gap-4 md:gap-8 md:h-56 flex-auto grid-cols-1 md:grid-cols-2">
22-
<div className="flex-auto relative h-56 md:h-full">
23-
<MarkupEditor />
24-
</div>
25-
26-
<div className="flex-auto h-56 md:h-full">
27-
<Preview />
28-
</div>
24+
<div className="flex flex-col h-auto md:h-full w-full">
25+
<div className="editor markup-editor gap-4 md:gap-8 md:h-56 flex-auto grid-cols-1 md:grid-cols-2">
26+
<div className="flex-auto relative h-56 md:h-full">
27+
<MarkupEditor markup={markup} dispatch={dispatch} />
2928
</div>
3029

31-
<div className="flex-none h-8" />
30+
<div className="flex-auto h-56 md:h-full">
31+
<Preview
32+
markup={markup}
33+
elements={result.elements}
34+
accessibleRoles={result.accessibleRoles}
35+
dispatch={dispatch}
36+
/>
37+
</div>
38+
</div>
3239

33-
<div className="editor gap-4 md:gap-8 md:h-56 flex-auto grid-cols-1 md:grid-cols-2 overflow-hidden">
34-
<div className="flex-auto relative h-56 md:h-full">
35-
<Query />
36-
</div>
40+
<div className="flex-none h-8" />
41+
42+
<div className="editor gap-4 md:gap-8 md:h-56 flex-auto grid-cols-1 md:grid-cols-2 overflow-hidden">
43+
<div className="flex-auto relative h-56 md:h-full">
44+
<Query query={query} result={result} dispatch={dispatch} />
45+
</div>
3746

38-
<div className="flex-auto h-56 md:h-full overflow-hidden">
39-
<Result />
40-
</div>
47+
<div className="flex-auto h-56 md:h-full overflow-hidden">
48+
<Result result={result} dispatch={dispatch} />
4149
</div>
4250
</div>
43-
</PlaygroundProvider>
51+
</div>
4452
);
4553
}
4654

src/components/Playground.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import React from 'react';
2+
import { render } from '@testing-library/react';
23
import Playground from './App';
3-
import { renderWithContext } from '../../tests/utils/render';
44

55
describe('App', () => {
66
it('should not throw on render', () => {
7-
renderWithContext(<Playground />);
7+
render(<Playground />);
88
});
99
});

src/components/Preview.js

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import React, { useState, useEffect, useRef } from 'react';
2-
import { usePlayground } from './Context';
32
import Scrollable from './Scrollable';
43
import PreviewHint from './PreviewHint';
54
import AddHtml from './AddHtml';
@@ -9,7 +8,7 @@ function selectByCssPath(rootNode, cssPath) {
98
return rootNode?.querySelector(cssPath.replace(/^body > /, ''));
109
}
1110

12-
function Preview() {
11+
function Preview({ markup, accessibleRoles, elements, dispatch }) {
1312
// Okay, listen up. `highlighted` can be a number of things, as I wanted to
1413
// keep a single variable to represent the state. This to reduce bug count
1514
// by creating out-of-sync states.
@@ -26,7 +25,6 @@ function Preview() {
2625
// Indicating that the `parsed` element can be highlighted again.
2726
const [highlighted, setHighlighted] = useState(false);
2827
const [roles, setRoles] = useState([]);
29-
const { state, dispatch } = usePlayground();
3028
const htmlRoot = useRef();
3129

3230
const { suggestion } = getQueryAdvise({
@@ -37,12 +35,12 @@ function Preview() {
3735
// TestingLibraryDom?.getSuggestedQuery(highlighted, 'get').toString() : null
3836

3937
useEffect(() => {
40-
setRoles(Object.keys(state.result.accessibleRoles || {}).sort());
41-
}, [state.result.accessibleRoles]);
38+
setRoles(Object.keys(accessibleRoles || {}).sort());
39+
}, [accessibleRoles]);
4240

4341
useEffect(() => {
4442
if (highlighted) {
45-
state.result.elements?.forEach((el) => {
43+
elements?.forEach((el) => {
4644
const target = selectByCssPath(htmlRoot.current, el.cssPath);
4745
target?.classList.remove('highlight');
4846
});
@@ -51,15 +49,15 @@ function Preview() {
5149
highlighted?.classList?.remove('highlight');
5250

5351
if (highlighted === false) {
54-
state.result.elements?.forEach((el) => {
52+
elements?.forEach((el) => {
5553
const target = selectByCssPath(htmlRoot.current, el.cssPath);
5654
target?.classList.add('highlight');
5755
});
5856
}
5957
}
6058

6159
return () => highlighted?.classList?.remove('highlight');
62-
}, [highlighted, state.result.elements]);
60+
}, [highlighted, elements]);
6361

6462
const handleClick = (event) => {
6563
if (event.target === htmlRoot.current) {
@@ -87,7 +85,7 @@ function Preview() {
8785
setHighlighted(target);
8886
};
8987

90-
return state.markup ? (
88+
return markup ? (
9189
<div
9290
className="w-full h-full flex flex-col relative overflow-hidden"
9391
onMouseEnter={() => setHighlighted(true)}
@@ -100,7 +98,7 @@ function Preview() {
10098
onClick={handleClick}
10199
onMouseMove={handleMove}
102100
ref={htmlRoot}
103-
dangerouslySetInnerHTML={{ __html: state.markup }}
101+
dangerouslySetInnerHTML={{ __html: markup }}
104102
/>
105103
</Scrollable>
106104
</div>
@@ -114,4 +112,4 @@ function Preview() {
114112
);
115113
}
116114

117-
export default Preview;
115+
export default React.memo(Preview);

src/components/PreviewHint.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ function PreviewHint({ roles, suggestion }) {
1818
);
1919
}
2020

21-
export default PreviewHint;
21+
export default React.memo(PreviewHint);

src/components/Query.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,17 @@
11
import React from 'react';
22
import QueryEditor from './QueryEditor';
33
import QueryOutput from './QueryOutput';
4-
import { usePlayground } from './Context';
5-
6-
function Query({ onChange, initialValue }) {
7-
const { state } = usePlayground();
84

5+
function Query({ query, result, dispatch }) {
96
return (
107
<div className="relative h-full w-full flex flex-col">
118
<div className="query-editor flex-auto relative overflow-hidden">
12-
<QueryEditor initialValue={initialValue} onChange={onChange} />
9+
<QueryEditor query={query} dispatch={dispatch} />
1310
</div>
1411

15-
<QueryOutput
16-
error={state.result.error?.message}
17-
result={state.result.formatted}
18-
/>
12+
<QueryOutput error={result.error?.message} result={result.formatted} />
1913
</div>
2014
);
2115
}
2216

23-
export default Query;
17+
export default React.memo(Query);

src/components/QueryEditor.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
import React, { useCallback } from 'react';
22
import Editor from './Editor';
3-
import { usePlayground } from './Context';
4-
5-
function QueryEditor() {
6-
const { dispatch, state } = usePlayground();
73

4+
function QueryEditor({ query, dispatch }) {
85
const onLoad = useCallback(
96
(editor) => dispatch({ type: 'SET_QUERY_EDITOR', editor }),
107
[dispatch],
@@ -18,11 +15,11 @@ function QueryEditor() {
1815
return (
1916
<Editor
2017
mode="javascript"
21-
initialValue={state.query}
18+
initialValue={query}
2219
onLoad={onLoad}
2320
onChange={onChange}
2421
/>
2522
);
2623
}
2724

28-
export default QueryEditor;
25+
export default React.memo(QueryEditor);

src/components/QueryOutput.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ function QueryOutput({ error, result }) {
1212
);
1313
}
1414

15-
export default QueryOutput;
15+
export default React.memo(QueryOutput);

src/components/Result.js

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,16 @@ import React from 'react';
22
import ErrorBox from './ErrorBox';
33
import ResultQueries from './ResultQueries';
44
import ResultSuggestion from './ResultSuggestion';
5-
import { usePlayground } from './Context';
65
import Scrollable from './Scrollable';
76

8-
function Result() {
9-
const { state } = usePlayground();
10-
11-
if (state.result.error) {
7+
function Result({ result, dispatch }) {
8+
if (result.error) {
129
return (
13-
<ErrorBox
14-
caption={state.result.error.message}
15-
body={state.result.error.details}
16-
/>
10+
<ErrorBox caption={result.error.message} body={result.error.details} />
1711
);
1812
}
1913

20-
if (!state.result.expression) {
14+
if (!result.expression) {
2115
return (
2216
<div className="space-y-4 text-sm">
2317
<div className="min-h-8">
@@ -39,17 +33,27 @@ function Result() {
3933
);
4034
}
4135

42-
const { data, suggestion } = state.result.elements?.[0] || {};
36+
const { data, suggestion } = result.elements?.[0] || {};
4337

4438
return (
4539
<div className="flex flex-col overflow-hidden w-full h-full">
4640
<div className="flex-none pb-4 border-b">
47-
<ResultSuggestion data={data} suggestion={suggestion} />
41+
<ResultSuggestion
42+
result={result}
43+
dispatch={dispatch}
44+
data={data}
45+
suggestion={suggestion}
46+
/>
4847
</div>
4948

5049
<div className="flex-auto">
5150
<Scrollable>
52-
<ResultQueries data={data} suggestion={suggestion} />
51+
<ResultQueries
52+
data={data}
53+
suggestion={suggestion}
54+
activeMethod={result.expression?.method}
55+
dispatch={dispatch}
56+
/>
5357
</Scrollable>
5458
</div>
5559
</div>

0 commit comments

Comments
 (0)