1
0
Fork 0
mirror of https://github.com/terribleplan/next.js.git synced 2024-01-19 02:48:18 +00:00

Merge branch 'master' into canary

# Conflicts:
#	.travis.yml
This commit is contained in:
Tim Neutkens 2017-11-15 13:20:46 +01:00
commit 6977d61ebe
20 changed files with 319 additions and 39 deletions

View file

@ -54,7 +54,13 @@ export default ComposedComponent => {
<ComposedComponent url={url} {...composedInitialProps} />
</ApolloProvider>
)
await getDataFromTree(app)
await getDataFromTree(app, {
router: {
query: context.query,
pathname: context.pathname,
asPath: context.asPath
}
})
// Extract query data from the Apollo's store
const state = apollo.getInitialState()

View file

@ -0,0 +1,41 @@
# Redux-Observable example
## How to use
Download the example [or clone the repo](https://github.com/zeit/next.js):
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/master | tar -xz --strip=2 next.js-master/examples/with-redux-observable
cd with-redux-observable
```
Install it and run:
```bash
npm install
npm run dev
```
### The idea behind the example
Example is a page that renders information about Star-Wars characters. It fetches new character
every 3 seconds having the initial character fetched on a server.
Example also uses `redux-logger` to log every action.
![demo page](demo.png)
The main problem with integrating Redux, Redux-Observable and Next.js is probably making initial requests
on a server. That's because it's not possible to wait until epics are resolved in `getInitialProps` hook.
In order to have best of two worlds, we can extract request logic and use it separately.
That's what `lib/api.js` is for. It keeps functions that return configured Observable for ajax request.
You can notice that `fetchCharacter` method is used to get initial data in `pages/index.js`
and also in `lib/reducer.js` within an epic.
Other than above, configuration is pretty the same as in
[with-redux example](https://github.com/zeit/next.js/tree/canary/examples/with-redux)
and [redux-observable docs](https://redux-observable.js.org/). There is, however one important thing
to note, that we are not using `AjaxObservable` from `rxjs` library because it doesn't work on Node.
Because of this we use a library like [universal-rx-request](https://www.npmjs.com/package/universal-rx-request).

View file

@ -0,0 +1,43 @@
import React from 'react'
import { connect } from 'react-redux'
const CharacterInfo = ({character, error, fetchCharacter, isFetchedOnServer = false}) => (
<div className='CharacterInfo'>
{
error ? <p>We encountered and error.</p>
: <article>
<h3>Character: {character.name}</h3>
<p>birth year: {character.birth_year}</p>
<p>gender: {character.gender}</p>
<p>skin color: {character.skin_color}</p>
<p>eye color: {character.eye_color}</p>
</article>
}
<p>
( was character fetched on server? -
<b>{isFetchedOnServer.toString()})</b>
</p>
<style jsx>{`
article {
background-color: #528CE0;
border-radius: 15px;
padding: 15px;
width: 250px;
margin: 15px 0;
color: white;
}
button {
margin-right: 10px;
}
`}</style>
</div>
)
export default connect(
state => ({
character: state.character,
error: state.error,
isFetchedOnServer: state.isFetchedOnServer
}),
)(CharacterInfo)

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View file

@ -0,0 +1,12 @@
/**
* Ajax actions that return Observables.
* They are going to be used by Epics and in getInitialProps to fetch initial data.
*/
import { ajax, Observable } from './rxjs-library'
import { fetchCharacterSuccess, fetchCharacterFailure } from './reducer'
export const fetchCharacter = (id, isServer) =>
ajax({ url: `https://swapi.co/api/people/${id}` })
.map(response => fetchCharacterSuccess(response.body, isServer))
.catch(error => Observable.of(fetchCharacterFailure(error.response.body, isServer)))

View file

@ -0,0 +1,17 @@
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { combineEpics, createEpicMiddleware } from 'redux-observable'
import starwarsReducer, { fetchUserEpic } from './reducer'
const rootEpic = combineEpics(
fetchUserEpic,
)
export default function initStore (initialState) {
const epicMiddleware = createEpicMiddleware(rootEpic)
const logger = createLogger({ collapsed: true }) // log every action to see what's happening behind the scenes.
const reduxMiddleware = applyMiddleware(thunkMiddleware, epicMiddleware, logger)
return createStore(starwarsReducer, initialState, reduxMiddleware)
};

View file

@ -0,0 +1,51 @@
import * as api from './api'
import { Observable } from './rxjs-library'
const FETCH_CHARACTER_SUCCESS = 'FETCH_CHARACTER_SUCCESS'
const FETCH_CHARACTER_FAILURE = 'FETCH_CHARACTER_FAILURE'
const START_FETCHING_CHARACTERS = 'START_FETCHING_CHARACTERS'
const STOP_FETCHING_CHARACTERS = 'STOP_FETCHING_CHARACTERS'
const INITIAL_STATE = {
nextCharacterId: 1,
character: {},
isFetchedOnServer: false,
error: null
}
export default function reducer (state = INITIAL_STATE, { type, payload }) {
switch (type) {
case FETCH_CHARACTER_SUCCESS:
return {
...state,
character: payload.response,
isFetchedOnServer: payload.isServer,
nextCharacterId: state.nextCharacterId + 1
}
case FETCH_CHARACTER_FAILURE:
return { ...state, error: payload.error, isFetchedOnServer: payload.isServer }
default:
return state
}
}
export const startFetchingCharacters = () => ({ type: START_FETCHING_CHARACTERS })
export const stopFetchingCharacters = () => ({ type: STOP_FETCHING_CHARACTERS })
export const fetchUserEpic = (action$, store) =>
action$.ofType(START_FETCHING_CHARACTERS)
.mergeMap(
action => Observable.interval(3000)
.mergeMap(x => api.fetchCharacter(store.getState().nextCharacterId))
.takeUntil(action$.ofType(STOP_FETCHING_CHARACTERS))
)
export const fetchCharacterSuccess = (response, isServer) => ({
type: FETCH_CHARACTER_SUCCESS,
payload: { response, isServer }
})
export const fetchCharacterFailure = (error, isServer) => ({
type: FETCH_CHARACTER_FAILURE,
payload: { error, isServer }
})

View file

@ -0,0 +1,13 @@
// we bundle only what is necessary from rxjs library
import 'rxjs/add/operator/mergeMap'
import 'rxjs/add/operator/map'
import 'rxjs/add/operator/delay'
import 'rxjs/add/operator/takeUntil'
import { Observable } from 'rxjs/Observable'
import 'rxjs/add/observable/interval'
import ajax from 'universal-rx-request' // because standard AjaxObservable only works in browser
export {
Observable,
ajax
}

View file

@ -0,0 +1,25 @@
{
"name": "with-redux-observable",
"version": "1.0.0",
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start"
},
"author": "tomaszmularczyk(tomasz.mularczyk89@gmail.com)",
"dependencies": {
"next": "latest",
"next-redux-wrapper": "^1.0.0",
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-redux": "^5.0.1",
"redux": "^3.6.0",
"redux-logger": "^3.0.6",
"redux-observable": "^0.17.0",
"redux-thunk": "^2.1.0",
"rxjs": "^5.5.2",
"superagent": "^3.8.1",
"universal-rx-request": "^1.0.3"
},
"license": "ISC"
}

View file

@ -0,0 +1,47 @@
import React from 'react'
import Link from 'next/link'
import withRedux from 'next-redux-wrapper'
import initStore from '../lib'
import { startFetchingCharacters, stopFetchingCharacters } from '../lib/reducer'
import * as api from '../lib/api'
import CharacterInfo from '../components/CharacterInfo'
class Counter extends React.Component {
static async getInitialProps ({ store, isServer }) {
const nextCharacterId = store.getState().nextCharacterId
const resultAction = await api.fetchCharacter(nextCharacterId, isServer).toPromise() // we need to convert observable to Promise
store.dispatch(resultAction)
return { isServer }
}
componentDidMount () {
this.props.startFetchingCharacters()
}
componentWillUnmount () {
this.props.stopFetchingCharacters()
}
render () {
return (
<div>
<h1>Index Page</h1>
<CharacterInfo />
<br />
<nav>
<Link href='/other'><a>Navigate to "/other"</a></Link>
</nav>
</div>
)
}
}
export default withRedux(
initStore,
null,
{
startFetchingCharacters,
stopFetchingCharacters
},
)(Counter)

View file

@ -0,0 +1,13 @@
import React from 'react'
import Link from 'next/link'
const OtherPage = () => (
<div>
<h1>Other Page</h1>
<Link href='/'>
<a>Get back to "/"</a>
</Link>
</div>
)
export default OtherPage

View file

@ -1 +1 @@
**/*.js
*.js

View file

@ -1,23 +1,14 @@
[![Deploy to now](https://deploy.now.sh/static/button.svg)](https://deploy.now.sh/?repo=https://github.com/zeit/next.js/tree/master/examples/with-typescript)
# TypeScript Next.js example
This is a really simple project that show the usage of Next.js with TypeScript.
## How to use it?
```
npm install # to install dependencies
npm run dev # to compile TypeScript files and to run next.js
npm install
npm run dev
```
Output JS files are aside the related TypeScript ones.
## To fix
In tsconfig.json the options `jsx="react"` compiles JSX syntax into nested React.createElement calls.
This solution doesn't work well with some Next.js features like `next/head` or `next/link`.
The workaround is to create JS files that just return the mentioned module and require them from TSX files.
Like
```js
import Link from 'next/link'
export default Link
```

View file

@ -1,6 +0,0 @@
import * as React from 'react'
export default () =>
<div>
<p>This is my component</p>
</div>

10
examples/with-typescript/next.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
declare module 'next/link' {
import { Url } from 'url'
export default class Link extends React.Component<
{
href: string | Url;
},
{}
> {}
}

View file

@ -2,16 +2,21 @@
"name": "with-typescript",
"version": "1.0.0",
"scripts": {
"dev": "concurrently \"tsc --watch\" next"
"dev": "concurrently \"tsc --pretty --watch\" \"next\"",
"prebuild": "tsc",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "latest",
"react": "^16.0.0",
"react-dom": "^16.0.0"
"react": "16.1.0",
"react-dom": "16.1.0"
},
"devDependencies": {
"@types/react": "^16.0.9",
"concurrently": "^3.1.0",
"typescript": "^2.1.5"
"@types/node": "8.0.51",
"@types/react": "16.0.22",
"concurrently": "^3.5.0",
"tslint": "5.8.0",
"typescript": "2.6.1"
}
}

View file

@ -0,0 +1 @@
export default () => <div>About us</div>

View file

@ -1,8 +1,9 @@
import * as React from 'react'
import MyComponent from '../components/MyComponent'
import Link from 'next/link'
export default () =>
export default () =>
<div>
<h1>Hello world</h1>
<MyComponent />
Hello World.{' '}
<Link href="/about">
<a>About</a>
</Link>
</div>

View file

@ -1,8 +1,8 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2015",
"jsx": "react",
"allowJs": true
}
"compilerOptions": {
"jsx": "react-native",
"module": "commonjs",
"strict": true,
"target": "es2017"
}
}

View file

@ -0,0 +1,10 @@
{
"defaultSeverity": "error",
"extends": ["tslint:recommended"],
"jsRules": {},
"rules": {
"quotemark": [true, "single", "jsx-double"],
"semicolon": [true, "never"]
},
"rulesDirectory": []
}