The source code of this case study is available in the
main branch of this
branch contains a few more features beyond this tutorial, such as a refined UI,
dark mode, caching with react-queries...
etc. You can try out the enhanced version right now with expo, see the
project page for instructions.
If you have any question or remarks regarding this tutorial, you're welcome in our Discord channel.
We'll put all the scrolling logic in a
Scroller class that we'll later use with hooks.
Create this new file:
Below is a summary of each member in this class.
The constructor takes a
ScrollViewref to enable the
A method to listen to selected entry changes. This will be useful in the table of content drawer to update the active entry on scroll.
A method to free a listener to selected entry changes.
A method to be used with
onLayoutin order to store the coordinates of each entry in the body of the article.
Event handlers to be passed to a
onScrollhandler will be used to update the selected entry in the table of content drawer.
A method to set the offset of the
headingscontainer. Because of the DOM structure offered by Docusaurus wich looks like:<article><header>...</header><div class="markdown"><h2>...</h2><h3>...</h3></div></article>
the computed headings tags coordinates will be relative to the
<div>rather then relative to the
ScrollViewcontent, and we need to adjust to that.
A method to imperatively scroll to the given entry name.
Scroller in a React Context#
Let's start by creating a scroller context and export the relevant hook and provider:
Then we can provide a
scroller instance from the
and scroll to the targeted entry on menu entry press.
Finally, we must consume the
scrollViewRef in the
and pass the
Scroller.handlers event handlers to the
Great! Nevertheless we have yet two unaddressed issues:
- Update selected entry on scroll in the
- Register headings layouts. We will use a custom renderer for that purpose.
Listening to Entry Changes in
First of all, I propose to factor the logic of adding a listener to
selected entry changes in a separate hook (
Then, we just need to consume this hook from the
Scroller is still missing the coordinates of each heading to be able to
scrollToEntry. For this purpose, we are going to create a custom
<h3> tags. We will also need to register a
to store the header height. If you remember well, the DOM has a structure like
Let's get back to
components/WebEngine.tsx and register both renderers here:
<header> tags have a content model set to
block, they will be rendered in a
View, so we can pass
Hence we're done with the tap-to-scroll feature! But the
ArticleBody is still
pretty ugly, so we'll use some styles and fixes to prettify it!
The avatar should be 50x50 and its container displayed in row. We are going to fix it in two steps:
- By targeting the container class with styles to display in row;
- By setting a custom
<img>renderer to fix the size.
So let's edit the
components/WebEngine to apply those fixes:
Fixing Paragraphs in
Paragraphs nested in
<li> elements have top and bottom margins, which is undesirable.
To fix the issue, we're going to add a custom
<p> renderer like so:
We are using
markers which contain the current nest level of
elements to assess if we are rendering inside a list. See
# anchors appended to Headings by Docusaurus#
These elements have a
"hash-link" class, so we can use
ignoreDomNode to discard them:
Great! Now the
# characters have been removed:
However, code samples look pretty ugly:
- They're missing padding;
- They should be horizontally scrollable and lines should not wrap;
- A monospace font should be used;
- Whitespaces should be preserved.
So, let's fix it!
Code samples are rendered by Docusaurus in a
<pre class="prism-code"> tag. We need to fix two issues:
- Define a custom renderer for
pretags, which renders inside a
ScrollViewwhen matching the
- Define a custom renderer for
spantags. We need to do that because the whole code block is rendered inside a
codeelement with a
display: flex; flex-direction: column. However,
codeis translated to a React Native
Textsince his element model is textual. To work around this issue, we can inject line breaks after each
spanelement with a CSS
token-lineclass which content does not end with a new line.
That's looking much better. We're almost done!
We could add a few more styles to match the React Native blog styles:
As a final note, I'd like to mention a few frustrating limitations in React Native that prevented me from replicating more accurately the official blog styles:
backgroundColorspans to the full line-box of text elements, whereas in CSS, it only spans to the text content-area. Below is a diagram explaining the difference: See a complete explanation in this excellent article on CSS text styling.
borderare ignored in nested text elements.
All these features are required to get the official blog appealing anchors styles:
Instead we have backgrounds overlapping each other, which becomes weird when there is a high density of anchors:
This is because, as stated before, the
backgroundColor spans to the entire
height of the line box, instead of spanning to the content area.
You can take a look at the
enhanced branch of the project and see how the
below features have been implemented:
- Cached queries with
- Dark mode (follows system mode);
- Progressive rendering for fast time to first contentful paint via
- Collapsible header with
- Video support with