pomber / didact Goto Github PK
View Code? Open in Web Editor NEWA DIY guide to build your own React
Home Page: https://pomb.us/build-your-own-react/
A DIY guide to build your own React
Home Page: https://pomb.us/build-your-own-react/
I think i might have come across some starvation issues for both the linked React and Didact demos on my machine.
In one scenario i open page and the section(with text content) below the solar system never gets to render, in another scenario they do get to render but only on cycles where i either switch tabs(enough idle time?) or some other factor contributing to freeing up CPU cycles for it to render(initial) or update.
The synchronous version does not share the same issue.
It might make sense to open an issue in React as well if the issue(assuming i'm not mistake) is not related to the implementation details of the specific demo.
Model Name: MacBook Air Model Identifier: MacBookAir6,1 Processor Name: Intel Core i5 Processor Speed: 1.3 GHz Number of Processors: 1 Total Number of Cores: 2 L2 Cache (per Core): 256 KB L3 Cache: 3 MB Memory: 4 GB Boot ROM Version: MBA61.0099.B33 SMC Version (system): 2.12f143 Serial Number (system): C02M32TSF5N8 Hardware UUID: B47DCA38-89A5-5C73-9E7D-4E47AEE0320B
const element = <Test />
const container = document.getElementById("root")
render(element, container)
const Test = (): JSX.Element => {
document.title = `Home Page`;
const [s, setS] = useState(10);
setTimeout(() => {
setS(Math.random()); // result is error in this moment
}, 5000);
return (
<div>
<h1 onClick={() => setS(c => c + 1)}>
Count: {s}
</h1>
</div>
);
}
export function useState(initial) {
...........
actions.forEach(action => hook.state = action(hook.state)); // here the action is a number and not the function which should set the value
// TypeError: action is not a function
........
same code just with directly called element
const element = Test();
const container = document.getElementById("root")
render(element, container)
export function useState(initial) {
// wipFiber is ##@@null
const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex];
use state not work if i call it from set timeout / interval
In Step III: Concurrent Mode, I learn that: requestIdleCallback(func) will call the call back function when the browser is idle.
But in workLoop function, that while loop confused me.
function workLoop(deadline) {
let shouldYield = false
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(
nextUnitOfWork
)
shouldYield = deadline.timeRemaining() < 1
}
requestIdleCallback(workLoop)
}
My question is: if shoudYield
is false, it means that the browser is busy, so we should stop calling the next unit of work.
But in this while loop, if shoudYield
is false, the loop will continue and do the next unit of work. Why?
Hello, I saw this repo for reverse-engineering or creating your own React library. It was a great project, but I have a question.
In this specific line:
https://github.com/pomber/didact/blob/master/didact.js#L136
function commitDeletion(fiber, domParent) {
if (fiber.dom) {
domParent.removeChild(fiber.dom)
} else {
commitDeletion(fiber.child, domParent)
}
}
How does commitDeletion
perform on sibling fibers of the given child fiber?
Hi. I’m wondering if you considered implementing the react key prop? Wondering what that looks like under the hood and haven’t found an approachable resource for it yet.
Thanks for the blog posts/implementation!
Awesome course.
Do you have the same course for vue?
Copied the whole code for a test and seems to be broken at the point where we try to do the actual work.
return (
<h1 onClick={() => setState(c => c + 1)}>
Count: {state}
)
At this point my browser says Uncaught SyntaxError: Unexpected token '<'at the point we want to render the html as JSX.
If my updated element is not the last child, it is pushed to the last due to appendChild. Although we delete the updated element's dom from the parent dom, we are still appending the updated element's dom to the end.
Eg
<div id="parent"> <h2 title="child">Chapter 4 (Reconciliation)</h2> {update ? <span id="span1">span1</span> : <div id="div1">div1</div>} <div id="div2">div2</div> <div>
In the above case, the span gets appended as the last child of the parent div instead of being the 2nd child when the update becomes true. Should we be using "repalceChild" instead?
let pendingEffects = []
function useEffect(fn, deps) {
const hook = {
tag: "EFFECT",
fn,
deps,
}
wipFiber._hooks.push(hook)
hookIndex++
}
if (isFunctionComponent) {
...
Object .keys(wipFiber._hooks)
.filter(hookIndex => wipFiber._hooks[hookIndex].tag === "EFFECT")
.forEach(hookIndex => {
const oldHook =
wipFiber.alternate &&
wipFiber.alternate._hooks &&
wipFiber.alternate._hooks[hookIndex]
const hook = wipFiber._hooks[hookIndex]
const depsChanged = (prev, next) => (_, index) => prev[index] !== next[index];
if (hook.deps.length === 0 && !oldHook
|| oldHook && (oldHook.deps.length !== hook.deps.length
|| oldHook && hook.deps.filter(depsChanged(oldHook.deps, hook.deps)).length !== 0)) {
pendingEffects.push(hook.fn)
}
})
...
...
pendingEffects.forEach(it => it()) // call pending effects after render
Thanks for your blog and code sharing, Didact is awesome!
I got an issue here when I tried to delete several siblings in the same time.
Lines 124 to 129 in a127ff4
In this line, after one fiber gets deleted, its sibling will commitWork
next, which has been already registered in deletions
.
As a result, some fiber will be deleted more than once (after its sibling and in deletions.forEach
), which will cause an error.
pragma and pragmaFrag cannot be set when runtime is automatic.
/** @jsx Didact.createElement */
const element = (
<div id="foo">
<a href="">bar</a>
<b></b>
</div>
)
"react-scripts": "3.0.0" was OK.
const SubTest = () => {
const [s, setS] = useState(10);
setTimeout(() => {
const newValue = Math.random();
console.log('----', newValue)
setS(s => newValue);
}, 1000);
return (
<h3> Count: {s} </h3>
);
}
const Test = (): JSX.Element => {
const [s, setS] = useState(10);
setTimeout(() => {
const newValue = Math.random();
console.log(newValue)
setS(s => newValue);
}, 2000);
return (
<div>
<h1>
Count: {s} {s1}
</h1>
<SubTest />
<SubTest />
</div>
);
}
// ---------------------
const MainCmp = (props: any) => {
return (
<div>{Test()}</div>
);
}
const element = <MainCmp />;
const container = document.getElementById("root")
render(element, container!)
Main (broken): the setter in Test component not change the state value (s, it is always 10), refresh the component but the value remain the initial.
Secondary (sometimes broken after 1st change): counter in SubTest sometimes work, sometimes break after first value change.
no console error, the newValue is correct, component is rerun each time just the state saving issue
somehow always a new hook created which not conneted with the dom and never deleted the old one so at end browser with freeze after 15-30 sec
I just followed the whole (new) tutorial on here https://pomb.us/build-your-own-react/ and I have to start off by saying amazing job. Super detailed explanations and the animations and display is obviously beautiful and stunning.
I have a question, though. Although most of the entire post is extremely in-depth, I felt that the useState
section was a little terse and I'm currently left unsure of how to utilize the setState
function.
Logically, it just doesn't quite make sense to me how to implement the setState
function given that each hook action is executed as action(hook.state)
. This would only be executing a function (function required) on the current state value. But I'm failing to see where the implementation would be of updating the current state.
That being said, another part where I felt was left out of this is calling render
again to update the state. I had noticed it was in one of the codesandbox examples but it doesn't seem to appear in the example code itself. Obviously this isn't a full react implementation so i wouldn't expect it to just "work" so it seems like the solution here is to include the render
function in any other function that would require a state update. My guess is that this would eventually just be tied into the overall state of the application and that would trigger a rerender.
Thanks for the guide and I look forward to any response
Hi. I have come across an issue. The following code is the only component I render:
function Counter() {
const [count, setCount] = Didact.useState(1);
return (
<div>
<h1>Count</h1>
<botton onClick={() => setCount((c) => c + 1)}>+</button>
<p>{count}</p>
<botton onClick={() => setCount((c) => c - 1)}>-</button>
</div>
);
}
This code gives me this error:
/src/index.js: Expected corresponding JSX closing tag for <botton> (278:54)
276 | <p onClick={() => setCount((c) => c + 1)}>+</p>
277 | <p>{count}</p>
278 | <botton onClick={() => setCount((c) => c - 1)}>-</button>
| ^
279 | </div>
280 | );
281 | }
But this is weird because if I use p
tag for buttons, everything works as expected.
i check this code and i think we need to set condition for when old Fiber is null this mean we are in first render!
Line 252 in fa79b34
Line 153 in fa79b34
Line 146 in fa79b34
we just rewrite a reconciler function
const reconcileChildren = (
fiber:Fiber,
children:(PreactElement|PreactTextElement)[]
) => {
let oldFiber = fiber.alternate && fiber.alternate.child
let prevSibling:Fiber|null = null
children.forEach((el,index)=>{
let newFiber:Fiber|null = null
if(oldFiber != null){
const sameType = oldFiber && el && el.type === oldFiber.type
if(sameType){
// in this condition element and oldFiber is exist and
// both types are equal maybe props are changed
newFiber = {
type:oldFiber.type,
props:el.props, // assign new props
dom:oldFiber.dom,
parent:fiber,
alternate:oldFiber,
effectTag:"UPDATE",
child:null,
sibling:null
}
}
if(el && !sameType ){
newFiber = {
type:el.type,
props:el.props,
dom:null,
parent:fiber,
alternate:null,
effectTag:"PLACEMENT",
child:null,
sibling:null
}
}
if(oldFiber && !sameType){
oldFiber.effectTag = "DELETION"
deletion.push(oldFiber)
}
if(oldFiber){
oldFiber = oldFiber.sibling
newFiber = {
type:el.type,
props:el.props,
dom:null,
parent:fiber,
alternate:null,
child:null,
sibling:null
}
}
if (index === 0){
fiber.child = newFiber
}else{
if(prevSibling){
prevSibling.sibling = newFiber
}
}
prevSibling = newFiber
}else{
// create a first render dom
newFiber = {
type:el.type,
props:el.props,
parent:fiber,
dom:null,
child:null,
sibling:null,
alternate:null
}
newFiber.alternate = newFiber
if (index === 0){
fiber.child = newFiber
}else{
if(prevSibling){
prevSibling.sibling = newFiber
}
}
prevSibling = newFiber
return
}
})
}
if i wrong please tell me :)
Didact.render(element1, container1);
Didact.render(element2, container2);
Expected Behaviour:
element1 should have been appended to the container1 and element2 should have been appended to the container2.
Current Behaviour:
Only element2 is being appended to container2.
Reason:
The second render call is executed before workLoop is executed for the first time as a result wipRoot and nextUnitOfWork change before the first render call gets the chance to render element1, container1.
Possible Fix:
let wipQueue = [];
function render(element, container) {
let tWipRoot;
tWipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot,
};
deletions = [];
wipQueue.push(tWipRoot);
}
function workLoop(deadline) {
let shouldYield = false;
if (!nextUnitOfWork && wipQueue) {
wipRoot = wipQueue.shift();
nextUnitOfWork = wipRoot;
}
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
Although this solution works, we also have to maintain another list of currentRoot for hooks to work.
Also, I am new to JS and front-end in general. So there might be a better solution, Just wanted to share my thoughts.
Hi there, I was having problems implementing the Didactjs so at some point I decided to just copy the intire file and a got an error
Uncaught TypeError: Cannot read properties of null (reading 'alternate')
function useState(initial) {
const oldHook =
wipFiber.alternate && // <- Error happened here
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex]
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [],
}
...
}
import Didact from './lib.mjs'
function App(props = { title: 'title' }){
const [ counter, setCounter ] = Didact.useState(0)
return Didact.createElement(
'div',
null,
Didact.createElement(
'h1',
null,
`${props.title}: ${counter}`
),
Didact.createElement(
'div',
null,
Didact.createElement(
'button',
{ onClick: () => console.log('click') },
'+'
),
Didact.createElement(
'button',
null,
'-'
)
)
)
}
Didact.render(App({ title: 'some' }), document.getElementById('root'))
I tried to make one more verification
const oldHook = wipFiber.alternate && wipFiber.alternate.hooks ...
const oldHook = wipFiber && wipFiber.alternate && wipFiber.alternate.hooks ...
But then i got another error:
Uncaught TypeError: Cannot read properties of null (reading 'hooks')
function useState(initial) {
...
wipFiber.hooks.push(hook) // <- Error happened here
hookIndex++
return [hook.state, setState]
}
Then I tried a bunch of another thing but got nowhere, so I hope find some answers here
in step 4 Fiber, prevSibling.sibling = newFiber;, fiber is the argument and prevSibling is the variable inside the function. How does the sibling come into fiber? Thank you.
sometimes children can be arrays:
<div>
{posts.map(post => <p>{post}</p>)}
</div>
this causes problems for didact.
the solution is to just flatten children:
reconcileChildren(fiber, fiber.props.children.flat())
Hi, thank you for your tutorial I am enjoying it. So far so good but I have confusion on this part.
if (index === 0) {
wipFiber.child = newFiber
} else if (element) {
// even we set this we are still going to overwrite it. so what's the point?
prevSibling.sibling = newFiber
}
// in here we are overwriting the whole object
// that means `prevSibling.sibling` will be gone
prevSibling = newFiber
Cheers!
function commitWork(fiber) {
if (!fiber) {
return
}
let domParentFiber = fiber.parent
while (!domParentFiber.dom) {
domParentFiber = domParentFiber.parent
}
const domParent = domParentFiber.dom
if (
fiber.effectTag === "PLACEMENT" &&
fiber.dom != null
) {
domParent.appendChild(fiber.dom)
} else if (
fiber.effectTag === "UPDATE" &&
fiber.dom != null
) {
updateDom(
fiber.dom,
fiber.alternate.props,
fiber.props
)
} else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent)
// function should return at this moment
return
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
let's call the fiber to be deleted oldFiber
if commitWork
function didnt return after commitDeletion(oldFiber, domParent)
, it will commitWork(oldFiber.child)
and commitWork(oldFiber.sibling)
.
This may cause a terrible bug, because u dont know what type is oldFiber.child.effectTag
or oldFiber.sibiling.effectTag
and commitWork
function dont know the two fibers are old fibers and will treat them as current fiber to process.
Hey there, I really appreciate your tutorial, great stuff.
That said, the way you handle reconciliation leads to bugs if things are conditionally rendered. I've forked your codesandbox to show the issue: https://codesandbox.io/s/didact-8-0l4lz. You should see that clicking the show / hide button will duplicate the count element. Note that if you first increment the count it works correctly, it's not obvious to me why that is.
Cheers!
Hey Rodrigo, thanks so much for providing this + tutorial so useful! - I've been looking for a really stripped back state management solution like this I can sideload on to my little js apps (the fact that it supports jsx is a bonus).
I'm actually building a JS library which is going into some software out of my control - and some of those users are going to require IE11 support (it's is a front end app going on a variety of website types)...
My thinking was, for those cases, just to recommend a bunch of polyfills (not caring about performance so much etc) to tick that box... But I've come across an issue I've spent so long on and can't figure out.
After loading up the usual polyfills (+ requestIdleCallback), it seems that the demo supplied - <Counter />
doesn't work.
When you click on it, the value just stays the same.
If you wrap the { state }
in a node like this: <span>{ state }</span>
it seems to work without issue... but naturally that's not practical and an odd restriction.
What I've managed to figure out is (maybe I've got it all wrong though...), somewhere in the reconcileChildren
function things are going wrong (for IE11), and what would be two sibling text nodes:
Count:
and 10
are merged into 1 dom reference:
Count: 10
I would usually not bother even trying to get this working on IE11 and move on, but it seems its about 1 line of code away from being polyfill-able...! (and at only a few kb this fits my needs perfectly)
Any ideas would be much appreciated...
And if it wasn't clear, I'm not enjoying asking about IE11... :)
How do I handle inline styling?
currently something like
<div style{{background:"black"}}></div>
gets converted to
<div style></div>
In commitWork
function, when entering DELETION
condition judgment,it should ruturn
;
)
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom)
+ return
}
commitWork(fiber.child)
commitWork(fiber.sibling)
}
Otherwise didact
will repeatedly delete child elements, and will throw a exception:
Here is my test code
/** @jsx Didact.createElement */
const container = document.getElementById("root")
const updateValue = e => {
rerender(e.target.value)
}
const rerender = value => {
const element = (
<div>
<input onInput={updateValue} value={value} />
<h2>Hello {value}</h2>
{
value === '1' ? <div><span>a</span><span>b</span></div> : <div><div>c</div><div>d</div></div>
}
</div>
)
Didact.render(element, container)
}
rerender("World")
The operation effect is as follows:
Test it on codesandbox
I open a PR to fix the question
Hi,
again, thanks for this very concise and understandable introduction.
One minor issue is this, it looks like
} else if (fiber.effectTag == UPDATE) {
should have been
} else if (fiber.effectTag == UPDATE && fiber.tag == HOST_COMPONENT) {
function reconcileChildren(wipFiber, elements) {
let index = 0;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
while (index < elements.length || oldFiber != null) {
const element = elements[index];
let newFiber = null;
const sameType = oldFiber && element && element.type == oldFiber.type;
if (sameType) {
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE"
};
}
if (element && !sameType) {
newFiber = {
type: element.type,
props: element.props,
dom: null,
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT"
};
}
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
index++;
}
}
When I use '!==' like
while (index < elements.length || oldFiber !== null) {
...
}
and When I input and then I can't continue typing, the browser is also stuck. Why ?
Awesome blog post! It has been the best guide that I've ever read and really helped me understand the internals of React Fiber architecture.
One thing that I notice is that inside the useState
, the setState
will always set nextUnitOfWork = wipRoot
, which means Didact will reconcile from the root. And inside updateFunctionComponent
, the function component will always call fiber.type(fiber.props)
which will cause the component re-render. But I believe this is not the case in React?
For example, we have this structure: https://playcode.io/1603467
<Parent>
<Children />
</Parent>
const Parent = ({ children }) => {
console.log('parent re-render')
return <div>{children}</div>
}
const Children = (props) => {
const [state, setState] = useState(0);
console.log('child re-render')
return <button onClick={() => setState(state+1)}>Click</button>
}
In actual React, when the <Children>
button is clicked and setState
is invoked, the <Parent>
component will not-rerender. It is reflected by the parent console.log
, which doesn't run again.
So my question is, how does Didact differ from React in this case? Which React's feature is not implemented in Didact?
Thanks
Any idea how implementate Routing like nextjs or react-router-dom?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.