32
loading...
This website collects cookies to deliver better user experience
foo = foo
to let it know that foo
changed in some unusual way. Usually you don't even need that.npm install use-immer
, and then we can start!body {
background-color: #444;
color: #fff;
font-family: monospace;
}
.command {
width: 80em;
margin-bottom: 1em;
}
.command textarea {
min-height: 5em;
width: 100%;
background-color: #666;
color: #fff;
font: inherit;
border: none;
padding: 4px;
margin: 0;
}
.command .output {
width: 100%;
min-height: 5em;
background-color: #666;
padding: 4px;
}
button {
background-color: #666;
color: #fff;
}
updateEntry
code, which gets part of an useImmer
-managed draft, and can do deep modifications to it.run
, deleteThis
, and addNew
- and with useImmer
it's actually quite fine. I ended up not doing this, as App
also needs Run All
button, and having Run
in the App
, but Delete
and Add New
managed in the Command
component felt weird.import React from "react"
export default ({input, output, updateEntry, run, deleteThis, addNew}) => {
let handleChange = e => {
updateEntry(entry => entry.input = e.target.value)
}
let handleKey = (e) => {
if (e.key === "Enter" && e.metaKey) {
run()
}
}
return (
<div className="command">
<textarea
className="input"
onChange={handleChange} value={input}
onKeyDown={handleKey}
/>
<div className="output">{output}</div>
<div>
<button onClick={run}>Run</button>
<button onClick={deleteThis}>Delete</button>
<button onClick={addNew}>Add New</button>
</div>
</div>
)
}
App
component is pretty big, so let's cover it piece by piece.run={run(index)}
instead of more usual run={(event) => run(index, event)}
. I think this is clearer, as template is already very busy, and too many =>
there make it very difficult to read.import React from "react"
import { useImmer } from "use-immer"
import CommandBox from "./CommandBox.js"
export default (props) => {
...
return (
<>
<h1>Notebook App</h1>
{notebook.map(({input,output}, index) => (
<CommandBox
key={index}
input={input}
output={output}
updateEntry={updateEntry(index)}
run={run(index)}
deleteThis={deleteThis(index)}
addNew={addNew(index)}
/>
))}
<div>
<button onClick={runAll}>Run All</button>
</div>
</>
)
}
useImmer
has very similar API to useState
:let [notebook, updateNotebook] = useImmer([
{ input: "print('Hello')", output: "" },
{ input: "print('World')", output: "" },
{ input: "print(f'2+2={2+2}')", output: "" },
])
updateEntry
. It's a curried function, which we take full advantage of by doing updateEntry={updateEntry(index)}
in the template.CommandBox
component only modifies the first argument of its callback. I also sent it draft
and index
because I thought addNew
and deleteThis
are going to be managed there, then I ended up not doing that, but I think it's fine to leave the API a bit more flexible. It's similar to how a lot of JavaScript callbacks pass extra index
argument that's usually ignored. For example .map(element => ...)
is really .map((element, index, array) => ...)
.let updateEntry = (index) => (cb) => {
updateNotebook(draft => {
cb(draft[index], draft, index)
})
}
let run = (index) => async () => {
let input = notebook[index].input
let output = await window.api.runScript("python3", input)
updateNotebook(draft => { draft[index].output = output })
}
let addNew = (index) => () => {
updateNotebook(draft => {
draft.splice(index + 1, 0, { input: "", output: "" })
})
}
let deleteThis = (index) => () => {
updateNotebook(draft => {
draft.splice(index, 1)
if (draft.length === 0) {
draft.push({ input: "", output: "" })
}
})
}
let runAll = async () => {
for (let index = 0; index < notebook.length; index++) {
await run(index)()
}
}