outlandishideas / kasia

:tophat: A React Redux toolset for the WordPress API
MIT License
218 stars 15 forks source link
javascript kasia redux sagas universal-applications wordpress wordpress-api wp-api

kasia

A React Redux toolset for the WordPress API

Made with ❤ at @outlandish

npm version travis ci build Coverage Status


:sparkles: We welcome contributors!

:vertical_traffic_light: Issues are triaged using a traffic light system:

   #00ff00 small - quick tasks, great for beginner contributors
   #ffff00 medium - tasks with increased complexity, may take some time to implement
   #ff0000 large - big tasks that touch many parts of the library, will require commitment

Get started contributing here.


Get data from WordPress and into components with ease...

// e.g. Get a post by its slug
@connectWpPost('post', 'spongebob-squarepants')
export default class extends React.Component () {
  render () {
    const { post: spongebob } = this.props.kasia

    if (!spongebob) {
      return <p>{'Who lives in a pineapple under the sea?'}</p>
    }

    return <h1>{spongebob.title.rendered}!</h1>
    //=> Spongebob Squarepants!
  }
}

Features

Glossary

Requirements

Kasia suits applications that are built using these technologies:

Install

npm install kasia --save
yarn add kasia

Import

// ES2015
import kasia from 'kasia'
// CommonJS
var kasia = require('kasia')

Configure

Configure Kasia in three steps:

  1. Initialise Kasia with an instance of node-wpapi.

  2. Spread the Kasia reducer when creating the redux root reducer.

  3. Run the Kasia sagas after creating the redux-saga middleware.

A slimline example...

import { combineReducers, createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import kasia from 'kasia'
import wpapi from 'wpapi'

const wpai = new wpapi({ endpoint: 'http://wordpress/wp-json' })

const { kasiaReducer, kasiaSagas } = kasia({ wpapi })

const rootSaga = function * () {
  yield [...kasiaSagas]
}

const rootReducer = combineReducers({
  ...kasiaReducer
})

const sagaMiddleware = createSagaMiddleware()

export default function configureStore (initialState) {
  const store = createStore(
    rootReducer,
    initialState,
    applyMiddleware(sagaMiddleware)
  )

  sagaMiddleware.run(rootSaga)

  return store
}

Usage

kasia(options) : Object

Configure Kasia.

Returns an object containing the Kasia reducer and sagas.

const { kasiaReducer, kasiaSagas } = kasia({
  wpapi: new wpapi({ endpoint: 'http://wordpress/wp-json' })
})

The options object accepts:

Decorators

Things to keep in mind:

@connectWpPost(contentType, identifier) : Component

Connect a component to a single entity in WordPress, e.g. Post, Page, or custom content type.

Returns a connected component.

Example, using identifier derived from route parameter on props:

import React, { Component } from 'react'
import { Route } from 'react-router'
import { connectWpPost } from 'kasia/connect'
import { Page } from 'kasia/types'

@connectWpPost(Page, (props) => props.params.slug)
export default class Page extends Component {
  render () {
    const { query, page } = this.props.kasia

    if (!query.complete) {
      return <span>Loading...</span>
    }

    return <h1>{page.title}</h1>
  }
}

// Without decorator support
export default connectWpPost(Page, (props) => props.params.slug)(Post)

@connectWpQuery(queryFn, shouldUpdate) : Component

Connect a component to the result of an arbitrary WP-API query. Query is always made with ?embed query parameter.

Returns a connected component.

The component will request new data via queryFn if shouldUpdate returns true.

Entities returned from the query will be placed on this.props.kasia.entities under the same normalised structure as described in The Shape of Things.

Example, fetching the most recent "News" entities:

import React, { Component } from 'react'
import { Route } from 'react-router'
import { connectWpPost } from 'kasia/connect'

// Note the invocation of `embed` in the query chain
@connectWpQuery((wpapi, props) => {
  return wpapi.news().month(props.month).embed().get()
}, (thisProps, nextProps) => thisProps.month != nextProps.month)
export default class RecentNews extends Component {
  render () {
    const {
      query,
      data: { news }
    } = this.props.kasia

    if (!query.complete) {
      return <span>Loading...</span>
    }

    return (
      <div>
        <h1>Recent News Headlines</h1>
        {Object.keys(news).map((key) =>
          <h2>{news[key].title}</h2>)}
      </div>
    )
  }
}

// Without decorator support
export default connectWpQuery((wpapi) => {
  return wpapi.news().embed().get()
})(Post)

Exports

kasia

The Kasia configurator and preload utilities.

import kasia, { preload, preloadQuery } from 'kasia'

kasia/connect

The connect decorators.

import { connectWpPost, connectWpQuery } from 'kasia/connect'

kasia/types

The built-in WordPress content types that can be passed to connectWpPost to define what content type a request should be made for.

import {
  Category, Comment, Media, Page,
  Post, PostStatus, PostType,
  PostRevision, Tag, Taxonomy, User
} from 'kasia/types'

See Universal Application Utilities for more details.

Plugins

Kasia exposes a simple API for third-party plugins.

A plugin should:

// Example definition returned by a plugin
{
  reducer: {
    'kasia/SET_DATA': function setDataReducer () {}
    'kasia/REMOVE_DATA': function removeDataReducer () {}
  },
  sagas: [function * fetchDataSaga () {}]
}

A plugin can hook into Kasia's native action types, available at kasia/lib/constants/ActionTypes. All reducers for an action type are merged into a single function that calls each reducer in succession with the state returned by the previous reducer. This means the order of plugins that touch the same action type is important.

Available plugins:

Please create a pull request to get your own added to the list.

Universal Applications

Important...

Utilities

runSagas(store, sagas) : Promise

Run a bunch of sagas against the store and wait on their completion.

Returns a Promise resolving on completion of all the sagas.

preload(components[, renderProps][, state]) : Generator

Create a saga operation that will preload all data for any Kasia components in components.

Returns a saga operation.

preloadQuery(queryFn[, renderProps][, state]) : Generator

Create a saga operation that will preload data for an arbitrary query against the WP API.

Returns a saga operation.

<KasiaConnectedComponent>.preload(renderProps[, state]) : Array<Array>

Connected components expose a static method preload that produces an array of saga operations to facilitate the request for entity data on the server.

Returns an array of saga operations.

Saga Operation Signature

A saga operation is an array of the form:

[ sagaGeneratorFn, action ]

Where:

Example

A somewhat contrived example using the available preloader methods.

import { match } from 'react-router'
import { runSagas, preload, preloadQuery } from 'kasia'

import routes from './routes'
import store from './store'
import renderToString from './render'
import getAllCategories from './queries/categories'

export default function renderPage (res, location) { 
  return match({ routes, location }, (error, redirect, renderProps) => {
    if (error) return res.sendStatus(500)
    if (redirect) return res.redirect(302, redirect.pathname + redirect.search)

    // We are using `runSagas` which rewinds for us, but if we weren't then
    // we would call `kasia.rewind()` here instead:
    //
    // kasia.rewind()

    // Each preloader accepts the state that may/may not have been modified by
    // the saga before it, so the order might be important depending on your use-case!
    const preloaders = [
      () => preload(renderProps.components, renderProps),
      (state) => preloadQuery(getAllCategories, renderProps, state)
    ]

    return runSagas(store, preloaders)
      .then(() => renderToString(renderProps.components, renderProps, store.getState()))
      .then((document) => res.send(document))
  })  
}

Testing

Kasia components can be tested by:

An example:

export function postQuery (wpapi) {...}

export function shouldUpdate (thisProps, nextProps, state) {...}

export class Post extends Component {...}

@connectWpQuery(postQuery, shouldUpdate)
export default Post

You can then test the component without decoration, the query and the shouldUpdate function in isolation.

Contributing

All pull requests and issues welcome!

If you're not sure how to contribute, check out Kent C. Dodds' great video tutorials on egghead.io!

Author & License

kasia was created by Outlandish and is released under the MIT license.