Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

A simple time-ago component for [React].

## Usage:
**Now uses native-time-ago** - a zero-dependency library using the native Intl.RelativeTimeFormat API.

## Benefits:
- ✅ Zero dependencies (no more bundled locale files)
- ✅ Supports 100+ languages automatically via native browser API
- ✅ No locale maintenance needed - translations improve as browsers update

`react-timeago` is a very simple component that takes a `date` prop and returns a `time` element with live updating date in a time-ago format. The date will update only as often as needed. For timestamps below a minute away — every second, for timestamps up to 5 minutes away — every minute, and so on.

Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,12 @@
"url": "https://github.com/nmn/react-timeago/issues"
},
"homepage": "https://github.com/nmn/react-timeago",
"peerDependencies": {
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"dependencies": {
"native-time-ago": "1.0.1"
},
"devDependencies": {
"@babel/cli": "^7.27.0",
"@babel/preset-env": "^7.18.2",
Expand Down
112 changes: 20 additions & 92 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import * as React from 'react'
import { useEffect, useState } from 'react'
import dateParser from './dateParser'
import defaultFormatter from './defaultFormatter'
import timeAgo from 'native-time-ago'

export type { Formatter, Suffix, Unit } from './types'

export type Props = $ReadOnly<{
Expand All @@ -29,17 +29,10 @@ export type Props = $ReadOnly<{
* date: string | number | Date + * rendering.
*/
now?: () => number,
locale?: string,
...
}>

// Just some simple constants for readability
const MINUTE = 60
const HOUR = MINUTE * 60
const DAY = HOUR * 24
const WEEK = DAY * 7
const MONTH = DAY * 30
const YEAR = DAY * 365

const defaultNow = () => Date.now()

export default function TimeAgo({
Expand All @@ -48,9 +41,10 @@ export default function TimeAgo({
component = 'time',
live = true,
minPeriod = 0,
maxPeriod = WEEK,
maxPeriod = 60 * 60 * 24 * 7,
title,
now = defaultNow,
locale = 'en',
...passDownProps
}: Props): null | React.MixedElement {
const [timeNow, setTimeNow] = useState(now())
Expand All @@ -59,105 +53,39 @@ export default function TimeAgo({
if (!live) {
return
}
const tick = (): 0 | TimeoutID => {
const then = dateParser(date).valueOf()
if (!then) {
console.warn('[react-timeago] Invalid Date provided')
return 0
}
const seconds = Math.round(Math.abs(timeNow - then) / 1000)

const unboundPeriod =
seconds < MINUTE
? 1000
: seconds < HOUR
? 1000 * MINUTE
: seconds < DAY
? 1000 * HOUR
: 1000 * WEEK

const period = Math.min(
Math.max(unboundPeriod, minPeriod * 1000),
maxPeriod * 1000,
)

if (period) {
return setTimeout(() => {
setTimeNow(now())
}, period)
}

return 0
}
const timeoutId = tick()
return () => {
if (timeoutId) {
clearTimeout(timeoutId)
}
}
}, [date, live, maxPeriod, minPeriod, now, timeNow])

const intervalId = setInterval(() => {
setTimeNow(now())
}, 60000)

return () => clearInterval(intervalId)
}, [live, now])

useEffect(() => {
setTimeNow(now())
}, [date])
}, [date, now])

const Komponent = component
const then = dateParser(date).valueOf()
if (!then) {
const dateObj = date instanceof Date ? date : new Date(date)
const then = dateObj.getTime()

if (!then || isNaN(then)) {
return null
}

const seconds = Math.round(Math.abs(timeNow - then) / 1000)
const suffix = then < timeNow ? 'ago' : 'from now'

const [value, unit] =
seconds < MINUTE
? [Math.round(seconds), 'second']
: seconds < HOUR
? [Math.round(seconds / MINUTE), 'minute']
: seconds < DAY
? [Math.round(seconds / HOUR), 'hour']
: seconds < WEEK
? [Math.round(seconds / DAY), 'day']
: seconds < MONTH
? [Math.round(seconds / WEEK), 'week']
: seconds < YEAR
? [Math.round(seconds / MONTH), 'month']
: [Math.round(seconds / YEAR), 'year']

const passDownTitle =
typeof title === 'undefined'
? typeof date === 'string'
? date
: dateParser(date).toISOString().substring(0, 16).replace('T', ' ')
: dateObj.toISOString().substring(0, 16).replace('T', ' ')
: title

const spreadProps =
Komponent === 'time'
? { ...passDownProps, dateTime: dateParser(date).toISOString() }
? { ...passDownProps, dateTime: dateObj.toISOString() }
: passDownProps

const nextFormatter = (
value: number = value,
unit: Unit = unit,
suffix: Suffix = suffix,
epochMilliseconds: number = then,
nextFormatter: Formatter = defaultFormatter,
now: () => number = now,
) =>
defaultFormatter(value, unit, suffix, epochMilliseconds, nextFormatter, now)
const effectiveFormatter: Formatter = formatter || defaultFormatter

let content
try {
content = effectiveFormatter(value, unit, suffix, then, nextFormatter, now)
if (!content) {
content = defaultFormatter(value, unit, suffix, then, nextFormatter, now)
}
} catch (error) {
console.error('[react-timeago] Formatter threw an error:', error)
content = defaultFormatter(value, unit, suffix, then, nextFormatter, now)
}
const content = timeAgo(date, locale)

return (
<Komponent {...spreadProps} title={passDownTitle}>
Expand Down