How to Calculate the Reading Time of Body Content with React
https://niram.org/read/
{
name: "read",
title: "read",
type: "number",
description: "put number in minutes for reading time👇",
validation: (Rule) => Rule.required(),
},Schema:
import RenderReadingTime from "./RenderReadingTime";
export default {
name: "page",
title: "Pages",
type: "document",
fields: [
{
title: "Content",
name: "content",
type: "array",
of: [{ type: "block" }],
},
{
name: "reading",
title: "Reading Time",
type: "string",
inputComponent: RenderReadingTime,
},
],
};import React from "react";
import { Card, Stack, Inline, Badge } from "@sanity/ui";
import { FormField } from "@sanity/base/components";
import { withDocument } from "part:@sanity/form-builder";
import { useReadingTime } from "react-hook-reading-time";
export const RenderReadingTime = React.forwardRef((props, ref) => {
const { type, document } = props;
const filteredBlock = document.content
.filter((x) => x._type === "block")
.map((children) => children.children.map((texts) => texts.text));
const readingTime = useReadingTime(filteredBlock.join(" "));
return (
<FormField description={type.description} title={type.title}>
<Card padding={[3, 3, 3, 3]} shadow={1} radius={2} ref={ref}>
<Stack space={[3, 3, 3]}>
<Inline space={2}>
<Badge tone={"primary"}>{readingTime.text}</Badge>
<Badge>{`${readingTime.words} words`}</Badge>
</Inline>
</Stack>
</Card>
</FormField>
);
});
export default withDocument(RenderReadingTime);export const articleQuery = groq`*[_type == "article" && slug.current == $slug][0] {
...,
reading,
body[] {
...,
_type == "imageWithAltText" => {
asset -> {
...,
url,
},
},
_type == "video" => {
asset -> {
...,
videoPlatform,
href,
},
},
markDefs[] {
...,
_type == "internalLink" => {
"path": reference->slug.current
}
},
},
'comments': *[
_type == "comment" &&
article._ref == ^._id &&
approved == true
],
user -> {
name,
featuredImage,
},
"slug": slug.current,
}`;reading->,?
export const articleQuery = groq`*[_type == "article" && slug.current == $slug][0] {
...,
reading->,
body[] {
...,
_type == "imageWithAltText" => {
asset -> {
...,
url,
},
},
_type == "video" => {
asset -> {
...,
videoPlatform,
href,
},
},
markDefs[] {
...,
_type == "internalLink" => {
"path": reference->slug.current
}
},
},
'comments': *[
_type == "comment" &&
article._ref == ^._id &&
approved == true
],
user -> {
name,
featuredImage,
},
"slug": slug.current,
}`;PatchEventfrom
"@sanity/form-builder/PatchEvent"into the RenderReadingTime component. I will try and update the component a bit later today!
This is what I have now but GROQ returns 1d.
import React from "react";
import { Card, Stack, Inline, Badge, TextInput } from "@sanity/ui";
import { FormField } from "@sanity/base/components";
import { withDocument } from "part:@sanity/form-builder";
import { useReadingTime } from "react-hook-reading-time";
import { useId } from "@reach/auto-id"; // hook to generate unique IDs
import PatchEvent, { set, unset } from "@sanity/form-builder/PatchEvent";
export const RenderReadingTime = React.forwardRef((props, ref) => {
const {
type,
document,
value,
readOnly,
onChange,
compareValue,
markers,
presence,
} = props;
const filteredBlock = document.body
.filter((x) => x._type === "block")
.map((children) => children.children.map((texts) => texts.text));
const readingTime = useReadingTime(filteredBlock.join(" "));
// Creates a unique ID for our input
const inputId = useId();
// Creates a change handler for patching data
const handleChange = React.useCallback(
// useCallback will help with performance
(event) => {
const inputValue = event.currentTarget.value; // get current value
// if the value exists, set the data, if not, unset the data
onChange(PatchEvent.from(inputValue ? set(inputValue) : unset()));
},
[]
);
return (
<FormField
description={type.description}
title={type.title}
compareValue={compareValue}
inputId={inputId}
__unstable_markers={markers}
__unstable_presence={presence}
>
<Card padding={[3, 3, 3, 3]} shadow={1} radius={2} ref={ref}>
<Stack space={[3, 3, 3]}>
<Inline space={2}>
<Badge tone="primary">{readingTime.text}</Badge>
<Badge>{`${readingTime.words} words`}</Badge>
<TextInput
id={inputId}
onChange={handleChange}
value={readingTime.minutes || ""}
readOnly={readOnly}
placeholder={readingTime.minutes}
ref={ref}
/>
</Inline>
</Stack>
</Card>
</FormField>
);
});
export default withDocument(RenderReadingTime);Although this works, I’m unsure if I’m following the best practice in terms of patching data! I ended up using the
sanityClientrather than
PatchEventas I believe PatchEvent requires an onChange event to trigger, and as the component doesn’t have a direct user input, this isn’t possible (correct me if I’m wrong).
The reading time component supports both
stringand
blocktypes, and requires the options of
fieldName(which field that will be patched, which is probably is the same field) & `target`:
{
title: "Content",
name: "content",
type: "array",
of: [{ type: "block" }],
},
{
name: "reading",
title: "Reading Time",
type: "string",
inputComponent: RenderReadingTime,
options: { fieldName: "reading", target: "content" }
},import React, { useState, useMemo, useEffect } from "react";
import { Card, Stack, Inline, Badge } from "@sanity/ui";
import { FormField } from "@sanity/base/components";
import { withDocument } from "part:@sanity/form-builder";
import { useReadingTime } from "react-hook-reading-time";
import sanityClient from "part:@sanity/base/client";
const client = sanityClient.withConfig({ apiVersion: "2022-02-15" });
export const RenderReadingTime = React.forwardRef((props, ref) => {
const { type, document } = props;
const [time, setTime] = useState(null);
const valid = useMemo(() => {
const validate =
(type.options?.target !== undefined &&
document[type.options?.target] !== undefined &&
document[type.options?.fieldName] !== undefined &&
Array.isArray(document[type.options?.target])) ||
Object.prototype.toString.call(document[type.options?.target]) === "[object String]";
const targetType = validate
? Array.isArray(document[type.options?.target])
? "block"
: "string"
: false;
return targetType;
}, []);
useEffect(() => {
client
.patch(document._id)
.set({ reading: time?.words && valid !== false ? time.text : "" })
.commit()
.catch((err) => console.error("The update to 'reading' failed: ", err.message));
}, [time]);
return (
<FormField description={type.description} title={type.title}>
<Card padding={[3, 3, 3, 3]} shadow={1} radius={2} ref={ref}>
<Stack space={[3, 3, 3]}>
{valid !== false ? (
<RenderReadingTimePatch
document={document}
type={type}
valid={valid}
time={time}
setTime={setTime}
/>
) : (
<Inline space={2}>
<Badge tone={"critical"}>Invalid or missing target/fieldName</Badge>
</Inline>
)}
</Stack>
</Card>
</FormField>
);
});
const RenderReadingTimePatch = ({ document, type, valid, time, setTime }) => {
const debouncedInput = useDebounce(document[type.options.target], 1000);
useEffect(() => {
if (valid === "block") {
const filteredBlock = document[type.options.target]
.filter((x) => x._type === "block")
.map((children) => children.children.map((texts) => texts.text));
setTime(useReadingTime(filteredBlock.join(" ")));
} else {
setTime(useReadingTime(useReadingTime(document[type.options.target])));
}
}, [debouncedInput]);
return (
<div>
{time && (
<Inline space={2}>
<Badge tone={"primary"}>{time.text}</Badge>
<Badge>{`${time.words} words`}</Badge>
</Inline>
)}
</div>
);
};
const useDebounce = (value, delay) => {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
};
export default withDocument(RenderReadingTime);time.textto
time.minutes.toString()
useEffect(() => {
client
.patch(document._id)
.set({ reading: time?.words && valid !== false ? time.text : "" }) //CHANGE HERE
.commit()
.catch((err) => console.error("The update to 'reading' failed: ", err.message));
}, [time]);.set({ reading: time?.words && valid !== false ? time.text : "" }).set({ reading: time?.words && valid !== false ? time.minutes.toString() : "" })Was this answer helpful?
Sanity – Build the way you think, not the way your CMS thinks
Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.