Skip to content
This repository was archived by the owner on Jun 26, 2026. It is now read-only.
Merged
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: 0 additions & 7 deletions .babelrc

This file was deleted.

14 changes: 0 additions & 14 deletions .eslintrc

This file was deleted.

25 changes: 25 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: CI

on:
pull_request:
push:
branches:
- master

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version:
- 20
- 22
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm test
- run: npm run build
20 changes: 0 additions & 20 deletions .travis.yml

This file was deleted.

72 changes: 32 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ Using [npm](https://www.npmjs.com/):

$ npm install reactjs-title

`react` and `prop-types` are peer dependencies, so install them in your
application if they are not already present.

Then with a module bundler like [webpack](https://webpack.github.io/) that supports either CommonJS or ES2015 modules, use as you would anything else:

```js
Expand All @@ -31,6 +34,11 @@ The UMD build is also available on [unpkg](https://unpkg.com):

You can find the library on `window.ReactTitle`.

```js
const Title = window.ReactTitle.default
const flushTitle = window.ReactTitle.flushTitle
```

### Usage

```js
Expand Down Expand Up @@ -60,47 +68,32 @@ It ends up looking something like this:

```js
import React from 'react'
import { render } from 'react-dom'
import { createRoot } from 'react-dom/client'
import Title from 'reactjs-title'

const App = React.createClass({

render() {
return (
<div>
<Title render="Awesome Website"/>
<DeeperPage/>
{/* ... */}
</div>
)
}

})

const DeeperPage = React.createClass({

getInitialState() {
return { profile: null }
},

componentDidMount() {
fetchProfile(profile => this.setState({ profile }))
},

render() {
// because it's a component, it gets to participate in the render
// lifecycle, updating the title as state changes...
const { profile } = this.state
const title = profile ? 'Loading...' : profile.fullName
return (
<div>
<Title render={parentTitle => `More Stuff | ${title}`}/>
{/* ... */}
</div>
)
}

})
function App() {
return (
<div>
<Title render="Awesome Website"/>
<DeeperPage/>
{/* ... */}
</div>
)
}

function DeeperPage({ profile }) {
// because it's a component, it gets to participate in the render
// lifecycle, updating the title as state changes...
const title = profile ? profile.fullName : 'Loading...'
return (
<div>
<Title render={parentTitle => `More Stuff | ${title}`}/>
{/* ... */}
</div>
)
}

createRoot(document.getElementById('root')).render(<App/>)
```

If you're using React Router, you probably want all of your route
Expand Down Expand Up @@ -155,4 +148,3 @@ This could be worked around, but it seems like a strange use-case that
would complicate the code a bit.

Enjoy!

16 changes: 16 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module.exports = function configureBabel(api) {
const isEsm = api.env('esm')

return {
presets: [
[
'@babel/preset-env',
{
targets: isEsm ? { esmodules: true } : { node: '12' },
modules: isEsm ? false : 'commonjs'
}
],
'@babel/preset-react'
]
}
}
53 changes: 53 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const js = require('@eslint/js')
const babelParser = require('@babel/eslint-parser')
const react = require('eslint-plugin-react')

module.exports = [
js.configs.recommended,
{
files: [ '**/*.js' ],
ignores: [ 'lib/**', 'es6/**', 'umd/**', 'node_modules/**' ],
languageOptions: {
ecmaVersion: 2020,
sourceType: 'module',
parser: babelParser,
parserOptions: {
requireConfigFile: false,
babelOptions: {
configFile: './babel.config.js'
},
ecmaFeatures: {
jsx: true
}
},
globals: {
document: 'readonly',
window: 'readonly',
describe: 'readonly',
it: 'readonly',
beforeEach: 'readonly',
afterEach: 'readonly',
module: 'readonly',
require: 'readonly',
process: 'readonly',
globalThis: 'readonly',
__dirname: 'readonly'
}
},
plugins: {
react
},
rules: {
'no-unused-vars': [ 'error', { argsIgnorePattern: '^_' } ],
'react/jsx-uses-react': 'error',
'react/jsx-uses-vars': 'error',
'react/prop-types': 'off',
semi: [ 'error', 'never' ]
},
settings: {
react: {
version: 'detect'
}
}
}
]
76 changes: 50 additions & 26 deletions modules/__tests__/Title-test.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,48 @@
import expect from 'expect'
import React from 'react'
import { render } from 'react-dom'
import { act } from 'react'
import { createRoot } from 'react-dom/client'
import { renderToString } from 'react-dom/server'
import { expect } from 'expect'
import { afterEach, beforeEach, describe, it } from 'node:test'
import Title, { flushTitle } from '../Title'
import { findRenderedComponentWithType } from 'react-addons-test-utils'

globalThis.IS_REACT_ACT_ENVIRONMENT = true

describe('Title', () => {
let roots = []
let bInstance

beforeEach(() => {
flushTitle()
document.title = ''
roots = []
bInstance = null
})

afterEach(() => {
roots.forEach(root => {
act(() => {
root.unmount()
})
})
flushTitle()
})

const A = React.createClass({
class A extends React.Component {
render() {
return (
<div>
<Title render="A"/>
<B/>
<B ref={instance => { bInstance = instance }}/>
</div>
)
}
})
}

const B = React.createClass({
getInitialState() {
return { title: 'B' }
},
class B extends React.Component {
state = {
title: 'B'
}

render() {
return (
Expand All @@ -36,40 +52,48 @@ describe('Title', () => {
</div>
)
}
})
}

const C = React.createClass({
class C extends React.Component {
render() {
return (
<div>
<Title render={prev => `${prev} | C`}/>
</div>
)
}
})
}

it('renders the title', (done) => {
function renderApp(element) {
const div = document.createElement('div')
render(<A/>, div, () => {
expect(document.title).toEqual('A | B | C')
done()
const root = createRoot(div)
roots.push(root)

act(() => {
root.render(element)
})

return root
}

it('renders the title', () => {
renderApp(<A/>)
expect(document.title).toEqual('A | B | C')
})

it('handles state changes in the middle of a chain', (done) => {
it('handles state changes in the middle of a chain', () => {
// incidentally tests the previous and next instances
// not getting screwed up too.
const div = document.createElement('div')
render(<A/>, div, function () {
expect(document.title).toEqual('A | B | C')
const b = findRenderedComponentWithType(this, B)
b.setState({
renderApp(<A/>)
expect(document.title).toEqual('A | B | C')

act(() => {
bInstance.setState({
title: 'B Updated'
}, () => {
expect(document.title).toEqual('A | B Updated | C')
done()
})
})

expect(document.title).toEqual('A | B Updated | C')
})

describe('server rendering with flushTitle', () => {
Expand Down
Loading
Loading