Basic Model: Output is the result of a pure function based on state
import './App.css'
export default function App({ count }) {
return (
<>
<h1>Vite + React</h1>
<div className="card">
<button>
count is {count}
</button>
</div>
</>
)
}
import { useState } from 'react'
import './App.css'
export default function App() {
const [count, setCount] = useState(0)
return (
<>
<h1>Vite + React</h1>
<div className="card">
<button onClick={() => setCount((count) => count + 1)}>
count is {count}
</button>
</div>
</>
)
}
Reactive Model: Hooks
import { useState, useEffect } from 'react'
import './App.css'
function useAutoCounter(ms) {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => setCount((c) => c + 1), ms);
return () => clearInterval(id);
}, [ms]);
return count;
}
export default function App() {
const count = useAutoCounter(1000);
return (
<>
<h1>Vite + React</h1>
<div className="card">
<button>
count is {count}
</button>
</div>
</>
)
}
Reactive Model: Hooks
export default function App({ orderDetails }) {
// Oops, should use `useCallback`!
const handleSubmit = (orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails,
});
};
// Should also useMemo for this expensive operation
const processedOrder = process(orderDetails);
return (
<>
<button onClick={() => handleSubmit(processedOrder)}>
Send Order #{processedOrder.id}
</button>
</>
);
}
Example Issue 1: Hooks are called very frequently
// Make a memoized version of process order, making it easier to
// use and compose in functions
function useProcessedOrder(orderDetails) {
return useMemo(() => process(orderDetails), [orderDetails]);
}
function useHandleSubmit(orderDetails) {
// Ooh! We can reuse our memoized hook here! Right?
// Wrong, this will lead to multiple calls to process
// even if the order details are the same
const processedOrder = useProcessedOrder(orderDetails);
return useCallback(() => handleSubmit(processedOrder), [processedOrder]);
}
export default function App({ orderDetails }) {
const handleSubmit = useHandleSubmit(orderDetails);
const processedOrder = useProcessedOrder(orderDetails);
return (
<>
<button onClick={() => handleSubmit(processedOrder)}>
Send Order #{processedOrder.id}
</button>
</>
);
}
Example Issue 2: Hook state cannot be reused
export default function Profile({ userId }) {
const { data: user, isLoading } = useQuery(
['user', userId],
() => fetchUser(userId),
{
enabled: !!userId,
}
);
return isLoading ? (
<p>Loading...</p>
) : (
<>
<UserAvatar user={user} />
<UserStats user={user} />
<UserPosts user={user} />
<UserFollowers user={user} />
<UserFollowing user={user} />
<UserSettings user={user} />
<UserNotifications user={user} />
<UserGroups user={user} />
<UserFeed user={user} />
</>
);
}
Example Issue 3: Hooks cannot be passed around
export default async function fetchUserProfileAndPosts(username) {
const userRes = await fetchJSON(
`/api/users/${username}`
);
const userProfile = await fetchJSON(
`/api/users/${userDetails.id}/profile`
);
const userPosts = await fetchJSON(
`/api/users/${userDetails.id}/posts`
);
return { userDetails, userProfile, userPosts };
}
What's a monad?
export default async function fetchUserProfileAndPosts(username) {
const userDetails = await fetchJSON(
`/api/users/${username}`
);
const userProfile = await fetchJSON(
`/api/users/${userDetails.id}/profile`
);
const userPosts = await fetchJSON(
`/api/users/${userDetails.id}/posts`
);
return { userDetails, userProfile, userPosts };
}
Imagine async without promises
This is how hooks work!
export default async function fetchUserProfileAndPosts(username) {
const {
data: userDetails,
isLoading: userDetailsLoading,
} = await useUserQuery(username);
const {
data: userProfile,
isLoading: userProfileLoading,
} = await useUserProfile(userDetails?.id);
const {
data: userPosts,
isLoading: userPostsLoading,
} = await useUserPosts(userDetails?.id);
const isLoading =
userDetailsLoading ||
userProfileLoading ||
userPostsLoading;
return {
userDetails,
userProfile,
userPosts,
isLoading,
};
}
Imagine async without promises
This is how hooks work!
Ideal design:
const lineItems = state([
{
id: 1,
name: 'Line Item 1',
price: 100,
quantity: 1,
},
{
id: 2,
name: 'Line Item 2',
price: 200,
quantity: 2,
},
]);
const useTotal = computed(() => {
return lineItems.reduce((total, item) =>
total + item.price * item.quantity,
0
);
});
export function ReceiptTotal() {
return (
<div>
<p>Total: {useTotal()}</p>
</div>
)
}
Basic Signals
const useUserDetails = asyncComputed(async (username) => {
return useFetchJSON(`/api/users/${username}`);
});
const useUserProfile = asyncComputed(async (userId) => {
return useFetchJSON(`/api/users/${userId}/profile`);
});
const useUserPosts = asyncComputed(async (userId) => {
return useFetchJSON(`/api/users/${userId}/posts`);
});
export const useUserProfileAndPosts = asyncComputed(
async (username) => {
const userDetails = useUserQuery(username).await();
const userProfile = useUserProfile(userDetails.id).await();
const userPosts = useUserPosts(userDetails.id).await();
return { userDetails, userProfile, userPosts };
}
);
export const useUserProfileAndPostsWithDefaults = computed((username) => {
const { data, isPending } = useUserProfileAndPosts(username);
if (isPending) {
return {
userDetails: null,
userProfile: null,
userPosts: null,
};
}
return data;
});
Async Signals!
const useAutoCounter = subscription((state, ms) => {
const id = setInterval(() => state.set(state.get() + 1), ms);
return () => clearInterval(id);
}, { initValue: 0 })
export default function App() {
const count = useAutoCounter(1000);
return (
<>
<h1>Vite + React</h1>
<div className="card">
<button>
count is {count}
</button>
</div>
</>
)
}
Subscriptions!