Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blog 27 - useTransition #29

Open
wants to merge 6 commits into
base: feat-lazy
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/hello-world/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export {default} from './lazy'
export {default} from './useTransition'
3 changes: 3 additions & 0 deletions examples/hello-world/src/useTransition/AboutTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function AboutTab() {
return <p>Welcome to my profile!</p>
}
11 changes: 11 additions & 0 deletions examples/hello-world/src/useTransition/ContactTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function ContactTab() {
return (
<>
<p>You can find me online here:</p>
<ul>
<li>[email protected]</li>
<li>+123456789</li>
</ul>
</>
)
}
23 changes: 23 additions & 0 deletions examples/hello-world/src/useTransition/PostsTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {memo} from 'react'

const PostsTab = memo(function PostsTab() {
// Log once. The actual slowdown is inside SlowPost.
console.log('[ARTIFICIALLY SLOW] Rendering 500 <SlowPost />')

let items = []
for (let i = 0; i < 500; i++) {
items.push(<SlowPost key={i} index={i} />)
}
return <ul className='items'>{items}</ul>
})

function SlowPost({index}) {
let startTime = performance.now()
while (performance.now() - startTime < 1) {
// Do nothing for 1 ms per item to emulate extremely slow code
}

return <li className='item'>Post #{index + 1}</li>
}

export default PostsTab
13 changes: 13 additions & 0 deletions examples/hello-world/src/useTransition/TabButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default function TabButton({children, isActive, onClick}) {
if (isActive) {
return <b>{children}</b>
}
return (
<button
onClick={() => {
onClick()
}}>
{children}
</button>
)
}
37 changes: 37 additions & 0 deletions examples/hello-world/src/useTransition/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {useState, useTransition} from 'react'
import TabButton from './TabButton.js'
import AboutTab from './AboutTab.js'
import PostsTab from './PostsTab.js'
import ContactTab from './ContactTab.js'
import './style.css'

export default function TabContainer() {
const [isPending, startTransition] = useTransition()
const [tab, setTab] = useState('about')

function selectTab(nextTab) {
startTransition(() => {
setTab(nextTab)
})
}

return (
<div>
<TabButton isActive={tab === 'about'} onClick={() => selectTab('about')}>
About
</TabButton>
<TabButton isActive={tab === 'posts'} onClick={() => selectTab('posts')}>
Posts (slow)
</TabButton>
<TabButton
isActive={tab === 'contact'}
onClick={() => selectTab('contact')}>
Contact
</TabButton>
<hr />
{tab === 'about' && <AboutTab />}
{tab === 'posts' && <PostsTab />}
{tab === 'contact' && <ContactTab />}
</div>
)
}
55 changes: 55 additions & 0 deletions examples/hello-world/src/useTransition/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
* {
box-sizing: border-box;
}

body {
font-family: sans-serif;
margin: 20px;
padding: 0;
}

h1 {
margin-top: 0;
font-size: 22px;
}

h2 {
margin-top: 0;
font-size: 20px;
}

h3 {
margin-top: 0;
font-size: 18px;
}

h4 {
margin-top: 0;
font-size: 16px;
}

h5 {
margin-top: 0;
font-size: 14px;
}

h6 {
margin-top: 0;
font-size: 12px;
}

code {
font-size: 1.2em;
}

ul {
padding-inline-start: 20px;
}

button {
margin-right: 10px;
}
b {
display: inline-block;
margin-right: 10px;
}
1 change: 0 additions & 1 deletion packages/react-dom/src/synthetic_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ fn trigger_event_flow(paths: Vec<Function>, se: &Event) {

fn dispatch_event(container: &Element, event_type: String, e: &Event) {
if e.target().is_none() {
log!("Target is none");
return;
}

Expand Down
8 changes: 2 additions & 6 deletions packages/react-reconciler/src/begin_work.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ pub fn begin_work(
work_in_progress: Rc<RefCell<FiberNode>>,
render_lane: Lane,
) -> Result<Option<Rc<RefCell<FiberNode>>>, JsValue> {
log!("begin_work {:?}", work_in_progress.clone());
unsafe {
DID_RECEIVE_UPDATE = false;
};
Expand Down Expand Up @@ -96,7 +95,7 @@ pub fn begin_work(
}
}

work_in_progress.borrow_mut().lanes = Lane::NoLane;
work_in_progress.borrow_mut().lanes -= render_lane.clone();
// if current.is_some() {
// let current = current.clone().unwrap();
// current.borrow_mut().lanes = Lane::NoLane;
Expand Down Expand Up @@ -280,7 +279,7 @@ fn update_suspense_component(
let next_primary_children = derive_from_js_value(&next_props, "children");
let next_fallback_children = derive_from_js_value(&next_props, "fallback");
push_suspense_handler(work_in_progress.clone());
log!("show_fallback {:?}", show_fallback);

if current.is_none() {
if show_fallback {
return Some(mount_suspense_fallback_children(
Expand Down Expand Up @@ -405,9 +404,6 @@ fn update_function_component(
render_with_hooks(work_in_progress.clone(), Component, render_lane.clone())?;

let current = { work_in_progress.borrow().alternate.clone() };
log!("{:?} {:?}", work_in_progress.clone(), unsafe {
DID_RECEIVE_UPDATE
});
if current.is_some() && unsafe { !DID_RECEIVE_UPDATE } {
bailout_hook(work_in_progress.clone(), render_lane.clone());
return Ok(bailout_on_already_finished_work(
Expand Down
10 changes: 5 additions & 5 deletions packages/react-reconciler/src/child_fiber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -389,11 +389,11 @@ fn reconcile_children_array(
for (_, fiber) in existing_children {
delete_child(return_fiber.clone(), fiber, should_track_effects);
}
log!(
"first_new_fiber {:?} {:?}",
first_new_fiber,
first_new_fiber.clone().unwrap().borrow().sibling
);
// log!(
// "first_new_fiber {:?} {:?}",
// first_new_fiber,
// first_new_fiber.clone().unwrap().borrow().sibling
// );
first_new_fiber
}

Expand Down
4 changes: 2 additions & 2 deletions packages/react-reconciler/src/fiber.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ impl Debug for FiberNode {
self.flags,
self.subtree_flags,
self.lanes,
self.child_lanes
self.child_lanes,
)
.expect("print error");
}
Expand Down Expand Up @@ -299,7 +299,7 @@ pub struct FiberRootNode {
pub finished_lanes: Lane,
pub suspended_lanes: Lane,
pub pinged_lanes: Lane, // Records the processed suspended lanes, comes from suspended lanes
pub callback_node: Option<Task>,
pub callback_node: Option<Rc<RefCell<Task>>>,
pub callback_priority: Lane,
pub pending_passive_effects: Rc<RefCell<PendingPassiveEffects>>,
pub ping_cache: Option<HashMap<JsValueKey, Rc<RefCell<HashSet<Lane>>>>>,
Expand Down
62 changes: 61 additions & 1 deletion packages/react-reconciler/src/fiber_hooks.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::cell::RefCell;
use std::rc::Rc;

use react::current_batch_config::REACT_CURRENT_BATCH_CONFIG;
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
use wasm_bindgen::{JsCast, JsValue};
use web_sys::js_sys::{Array, Function, Object, Reflect};
Expand Down Expand Up @@ -157,6 +158,18 @@ fn update_hooks_to_dispatcher(is_update: bool) {
.clone();
use_context_closure.forget();

// use_transition
let use_transition_closure = Closure::wrap(Box::new(if is_update {
update_transition
} else {
mount_transition
}) as Box<dyn Fn() -> Vec<JsValue>>);
let use_transition = use_transition_closure
.as_ref()
.unchecked_ref::<Function>()
.clone();
use_transition_closure.forget();

// use
let use_closure =
Closure::wrap(Box::new(_use) as Box<dyn Fn(JsValue) -> Result<JsValue, JsValue>>);
Expand All @@ -171,6 +184,8 @@ fn update_hooks_to_dispatcher(is_update: bool) {
.expect("TODO: panic set use_callback");
Reflect::set(&object, &"use_context".into(), &use_context)
.expect("TODO: panic set use_context");
Reflect::set(&object, &"use_transition".into(), &use_transition)
.expect("TODO: panic set use_transition");
Reflect::set(&object, &"use".into(), &use_fn).expect("TODO: panic set use");

updateDispatcher(&object.into());
Expand Down Expand Up @@ -604,7 +619,6 @@ fn update_effect(create: Function, deps: JsValue) {
.unwrap()
.borrow_mut()
.flags |= Flags::PassiveEffect;
log!("CURRENTLY_RENDERING_FIBER.as_ref().unwrap().borrow_mut()");

hook.as_ref().unwrap().clone().borrow_mut().memoized_state =
Some(MemoizedState::Effect(push_effect(
Expand Down Expand Up @@ -774,3 +788,49 @@ pub fn reset_hooks_on_unwind(wip: Rc<RefCell<FiberNode>>) {
WORK_IN_PROGRESS_HOOK = None;
}
}

fn mount_transition() -> Vec<JsValue> {
let result = mount_state(&JsValue::from(false)).unwrap();
let is_pending = result[0].as_bool().unwrap();
let set_pending = result[1].clone().dyn_into::<Function>().unwrap();
let hook = mount_work_in_progress_hook();
let set_pending_cloned = set_pending.clone();
let closure = Closure::wrap(Box::new(move |callback: Function| {
start_transition(set_pending_cloned.clone(), callback);
}) as Box<dyn Fn(Function)>);
let start: Function = closure.as_ref().unchecked_ref::<Function>().clone();
closure.forget();
hook.as_ref().unwrap().clone().borrow_mut().memoized_state =
Some(MemoizedState::MemoizedJsValue(start.clone().into()));
vec![JsValue::from_bool(is_pending), start.into()]
}

fn update_transition() -> Vec<JsValue> {
let result = update_state(&JsValue::undefined()).unwrap();
let is_pending = result[0].as_bool().unwrap();
let hook = update_work_in_progress_hook();
if let MemoizedState::MemoizedJsValue(start) = hook
.as_ref()
.unwrap()
.clone()
.borrow()
.memoized_state
.as_ref()
.unwrap()
{
return vec![JsValue::from_bool(is_pending), start.into()];
}
panic!("update_transition")
}

fn start_transition(set_pending: Function, callback: Function) {
set_pending.call1(&JsValue::null(), &JsValue::from_bool(true));
let prev_transition = unsafe { REACT_CURRENT_BATCH_CONFIG.transition };

// low priority
unsafe { REACT_CURRENT_BATCH_CONFIG.transition = Lane::TransitionLane.bits() };
callback.call0(&JsValue::null());
set_pending.call1(&JsValue::null(), &JsValue::from_bool(false));

unsafe { REACT_CURRENT_BATCH_CONFIG.transition = prev_transition };
}
6 changes: 6 additions & 0 deletions packages/react-reconciler/src/fiber_lanes.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use bitflags::bitflags;
use react::current_batch_config::REACT_CURRENT_BATCH_CONFIG;
use scheduler::{unstable_get_current_priority_level, Priority};
use std::cell::RefCell;
use std::hash::{Hash, Hasher};
Expand All @@ -13,6 +14,7 @@ bitflags! {
const SyncLane = 0b0000000000000000000000000000001; // onClick
const InputContinuousLane = 0b0000000000000000000000000000010; // Continuous Trigger, example: onScroll
const DefaultLane = 0b0000000000000000000000000000100; // useEffect
const TransitionLane = 0b0000000000000000000000000001000;
const IdleLane = 0b1000000000000000000000000000000;
}
}
Expand Down Expand Up @@ -46,6 +48,10 @@ pub fn is_subset_of_lanes(set: Lane, subset: Lane) -> bool {
}

pub fn request_update_lane() -> Lane {
let is_transition = unsafe { REACT_CURRENT_BATCH_CONFIG.transition } != 0;
if is_transition {
return Lane::TransitionLane;
}
let current_scheduler_priority_level = unstable_get_current_priority_level();
let update_lane = scheduler_priority_to_lane(current_scheduler_priority_level);
update_lane
Expand Down
Loading