Create A WebView-free Blog App with React Native Render HTML, Part III
This article is the part III of the Create a WebView-free Blog App with React Native Render HTML serie. See also Part I and Part II.
tip
The source code of this case study is available in the main
branch of this
repo: jsamr/rnrh-blog
. The enhanced
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.
tip
If you have any question or remarks regarding this tutorial, you're welcome in our Discord channel.
#
Tap To Scroll FeatureScroller
Class#
The We'll put all the scrolling logic in a Scroller
class that we'll later use with hooks.
Create this new file: utils/Scroller.ts
:
Below is a summary of each member in this class.
constructor
The constructor takes a
ScrollView
ref to enable thescrollToEntry
method.addSelectedEntryListener
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.
removeSelectedEntryListener
A method to free a listener to selected entry changes.
registerScrollEntry
A method to be used with
onLayout
in order to store the coordinates of each entry in the body of the article.handlers
Event handlers to be passed to a
ScrollView
component. TheonScroll
handler will be used to update the selected entry in the table of content drawer.setOffset
A method to set the offset of the
headings
container. Because of the DOM structure offered by Docusaurus wich looks like:the computed headings tags coordinates will be relative to the
<div>
rather then relative to theScrollView
content, and we need to adjust to that.scrollToEntry
A method to imperatively scroll to the given entry name.
Scroller
in a React Context#
Sharing the Let's start by creating a scroller context and export the relevant hook and provider:
Then we can provide a scroller
instance from the ArticleScreen
component,
and scroll to the targeted entry on menu entry press.
Finally, we must consume the scrollViewRef
in the ArticleBody
component,
and pass the Scroller.handlers
event handlers to the ScrollView
component:
Great! Nevertheless we have yet two unaddressed issues:
- Update selected entry on scroll in the
TOC
; - Register headings layouts. We will use a custom renderer for that purpose.
TOC
#
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 (hooks/useOnEntryChangeEffect.ts
):
Then, we just need to consume this hook from the TOC
component:
#
Register Headings LayoutsThe Scroller
is still missing the coordinates of each heading to be able to
properly scrollToEntry
. For this purpose, we are going to create a custom
renderer
for <h2>
and <h3>
tags. We will also need to register a <header>
renderer
to store the header height. If you remember well, the DOM has a structure like
below:
Let's get back to components/WebEngine.tsx
and register both renderers here:
Because the <h2>
, <h3>
and <header>
tags have a content model set to
block, they will be rendered in a View
, so we can pass onLayout
in
​viewProps
prop.
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!
#
Styling Refinements#
Fixing the AvatarThe 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:
<li>
Elements#
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:
note
We are using markers
which contain the current nest level of ol
and ul
elements to assess if we are rendering inside a list. See Markers
.
#
anchors appended to Headings by Docusaurus#
Discard 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!
#
Fixing Code SamplesCode samples are rendered by Docusaurus in a <pre class="prism-code">
tag. We need to fix two issues:
- Define a custom renderer for
pre
tags, which renders inside aScrollView
when matching the"prism-code"
class. - Define a custom renderer for
span
tags. We need to do that because the whole code block is rendered inside acode
element with adisplay: flex; flex-direction: column
. However,code
is translated to a React NativeText
since his element model is textual. To work around this issue, we can inject line breaks after eachspan
element with a CSStoken-line
class which content does not end with a new line.
That's looking much better. We're almost done!
#
Final TouchWe could add a few more styles to match the React Native blog styles:
#
Epilogue#
Frustrating React Native Text LimitationsAs 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:
backgroundColor
spans 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.padding
andborder
are 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.
#
Going FurtherYou can take a look at the enhanced
branch of the project and see how the
below features have been implemented:
- Cached queries with
react-queries
; - Dark mode (follows system mode);
- Progressive rendering for fast time to first contentful paint via
FlatList
; - Collapsible header with
react-native-reanimated
(v2); - Video support with
expo-av
.
That's all for this tutorial! Don't forget to give us a star if you enjoy this library. You can also follow me on Twitter, and rate this library on Open Base.