Screen orientation describes how the screen is currently rotated, using a type such as portrait-primary and an angle such as 0. Build useOrientation so a component can read that pair and update when the Screen Orientation API reports a change. The hook must also return a predictable fallback during server rendering or in browsers that do not expose the API.
type Orientation = {
type: string;
angle: number;
};
function useOrientation(
defaultValue?: Orientation // { type: 'portrait-primary', angle: 0 }
): Orientation;
// screen.orientation.type === 'landscape-primary'
// screen.orientation.angle === 90
useOrientation();
// { type: 'landscape-primary', angle: 90 }
// The Screen Orientation API is unavailable.
useOrientation({ type: 'landscape-secondary', angle: 270 });
// { type: 'landscape-secondary', angle: 270 }
screen.orientation.type and screen.orientation.angle as one object.change event and freshly read both properties inside the handler.screen or screen.orientation is unavailable, return defaultValue without throwing.window.orientation, listen for device-motion events, or add a media-query fallback.You will mirror the browser-owned orientation pair in React state while keeping the first render safe everywhere.
A reading layout might move controls when a tablet rotates from portrait to landscape. The browser already exposes the current type and angle through screen.orientation, but React does not rerender merely because those properties change. Your hook must bridge the browser event into state without assuming that a screen API exists during rendering.
Think of the fallback as a safe value for the render phase. After the component mounts, a supported browser becomes the source of truth. React state is only the rendered copy of that source.
const { useState } = require('react');
function useOrientation(defaultValue = { type: 'portrait-primary', angle: 0 }) {
const [value] = useState(() => {
if (typeof screen === 'undefined' || !screen.orientation) {
return defaultValue;
}
return {
type: screen.orientation.type,
angle: screen.orientation.angle,
};
});
return value;
}
This version can read the correct value once, but it creates no bridge from the browser back to React. Rotating the screen changes the browser object without changing React state, so the component keeps rendering the old orientation. It also reads the browser during render, which can make the server and browser begin with different output.
const { useEffect, useState } = require('react');
const DEFAULT_ORIENTATION = {
type: 'portrait-primary',
angle: 0,
};
function useOrientation(defaultValue = DEFAULT_ORIENTATION) {
// Rendering starts from caller-controlled data, so no browser global is needed.
const [orientation, setOrientation] = useState(defaultValue);
useEffect(() => {
const source = typeof screen === 'undefined' ? null : screen.orientation;
if (!source) {
return undefined;
}
// Capture the object once so cleanup targets the object used for setup.
const update = () => {
// Read both properties now; a mount-time snapshot would become stale.
setOrientation({
type: source.type,
angle: source.angle,
});
};
source.addEventListener('change', update);
// Reconcile after subscribing to close the render-to-effect timing gap.
update();
return () => {
source.removeEventListener('change', update);
};
}, []);
return orientation;
}
module.exports = { useOrientation };
The state initializer never touches a browser global, so server rendering gets the fallback. The effect acquires one ScreenOrientation object, subscribes once, and immediately reconciles state with its current properties. Capturing both source and update also guarantees that cleanup removes the exact handler from the exact object used during setup.
Call useOrientation({ type: 'portrait-primary', angle: 0 }) while a tablet is already in landscape-primary at 90 degrees.
screen read happens during rendering.screen.orientation and stores that object in source for the effect's whole lifetime.update to the source's change event, then calls update() once. That fresh read produces { type: 'landscape-primary', angle: 90 } and React rerenders.portrait-secondary and 180, then dispatches change. The stable handler reads both new values and publishes one new state object.removeEventListener('change', update) on the captured source. Later rotations no longer reach this hook instance.change event.screen, and a browser value can disagree with the server fallback. Fix: render from the fallback and reconcile in an effect.type and angle outside the handler makes later events replay stale data. Fix: read both from source inside every handler call.source used during setup.window.orientation answer different or obsolete questions. Fix: use the Screen Orientation object's own change event.screen.orientation.lock() for fullscreen experiences, accounting for permissions and browser support. It is deliberately outside this read-only hook.useSyncExternalStore when many components need one centralized browser subscription and an explicit server snapshot.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Screen orientation describes how the screen is currently rotated, using a type such as portrait-primary and an angle such as 0. Build useOrientation so a component can read that pair and update when the Screen Orientation API reports a change. The hook must also return a predictable fallback during server rendering or in browsers that do not expose the API.
type Orientation = {
type: string;
angle: number;
};
function useOrientation(
defaultValue?: Orientation // { type: 'portrait-primary', angle: 0 }
): Orientation;
// screen.orientation.type === 'landscape-primary'
// screen.orientation.angle === 90
useOrientation();
// { type: 'landscape-primary', angle: 90 }
// The Screen Orientation API is unavailable.
useOrientation({ type: 'landscape-secondary', angle: 270 });
// { type: 'landscape-secondary', angle: 270 }
screen.orientation.type and screen.orientation.angle as one object.change event and freshly read both properties inside the handler.screen or screen.orientation is unavailable, return defaultValue without throwing.window.orientation, listen for device-motion events, or add a media-query fallback.You will mirror the browser-owned orientation pair in React state while keeping the first render safe everywhere.
A reading layout might move controls when a tablet rotates from portrait to landscape. The browser already exposes the current type and angle through screen.orientation, but React does not rerender merely because those properties change. Your hook must bridge the browser event into state without assuming that a screen API exists during rendering.
Think of the fallback as a safe value for the render phase. After the component mounts, a supported browser becomes the source of truth. React state is only the rendered copy of that source.
const { useState } = require('react');
function useOrientation(defaultValue = { type: 'portrait-primary', angle: 0 }) {
const [value] = useState(() => {
if (typeof screen === 'undefined' || !screen.orientation) {
return defaultValue;
}
return {
type: screen.orientation.type,
angle: screen.orientation.angle,
};
});
return value;
}
This version can read the correct value once, but it creates no bridge from the browser back to React. Rotating the screen changes the browser object without changing React state, so the component keeps rendering the old orientation. It also reads the browser during render, which can make the server and browser begin with different output.
const { useEffect, useState } = require('react');
const DEFAULT_ORIENTATION = {
type: 'portrait-primary',
angle: 0,
};
function useOrientation(defaultValue = DEFAULT_ORIENTATION) {
// Rendering starts from caller-controlled data, so no browser global is needed.
const [orientation, setOrientation] = useState(defaultValue);
useEffect(() => {
const source = typeof screen === 'undefined' ? null : screen.orientation;
if (!source) {
return undefined;
}
// Capture the object once so cleanup targets the object used for setup.
const update = () => {
// Read both properties now; a mount-time snapshot would become stale.
setOrientation({
type: source.type,
angle: source.angle,
});
};
source.addEventListener('change', update);
// Reconcile after subscribing to close the render-to-effect timing gap.
update();
return () => {
source.removeEventListener('change', update);
};
}, []);
return orientation;
}
module.exports = { useOrientation };
The state initializer never touches a browser global, so server rendering gets the fallback. The effect acquires one ScreenOrientation object, subscribes once, and immediately reconciles state with its current properties. Capturing both source and update also guarantees that cleanup removes the exact handler from the exact object used during setup.
Call useOrientation({ type: 'portrait-primary', angle: 0 }) while a tablet is already in landscape-primary at 90 degrees.
screen read happens during rendering.screen.orientation and stores that object in source for the effect's whole lifetime.update to the source's change event, then calls update() once. That fresh read produces { type: 'landscape-primary', angle: 90 } and React rerenders.portrait-secondary and 180, then dispatches change. The stable handler reads both new values and publishes one new state object.removeEventListener('change', update) on the captured source. Later rotations no longer reach this hook instance.change event.screen, and a browser value can disagree with the server fallback. Fix: render from the fallback and reconcile in an effect.type and angle outside the handler makes later events replay stale data. Fix: read both from source inside every handler call.source used during setup.window.orientation answer different or obsolete questions. Fix: use the Screen Orientation object's own change event.screen.orientation.lock() for fullscreen experiences, accounting for permissions and browser support. It is deliberately outside this read-only hook.useSyncExternalStore when many components need one centralized browser subscription and an explicit server snapshot.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.