react-okr-tree Logoreact-okr-tree
指南

自定义节点内容

renderNode / nodeComponent / renderContent 三种写法与优先级,展开按钮与空态的定制口。

三种等价写法,优先级:renderNode > nodeComponent > renderContent > 内置 node.label

写法:

优先级 renderNode > nodeComponent > renderContent > node.label。点卡片选中节点,三种写法里都读得到node.isCurrent

renderContentxxx科技有有限公司这是一个有活力的公司
renderContent产品研发部这是一个有活力的产品研发部
renderContent研发-前端这是一个有活力的研发-前端
renderContent研发-后端这是一个有活力的研发-后端
renderContentUI 设计这是一个有活力的UI 设计
renderContent销售部这是一个有活力的销售部
renderContent销售一部这是一个有活力的销售一部
renderContent销售二部这是一个有活力的销售二部
renderContent财务部这是一个有活力的财务部

三种内容写法对比

实时切换三种写法,渲染结果一致。

查看源码
'use client'

import { useMemo, useState } from 'react'
import {
  OkrTree,
  type NodeComponent,
  type NodeComponentProps,
  type RenderContentFunction,
  type TreeNode,
} from 'react-okr-tree'
import 'react-okr-tree/style.css'
import { contentData } from './data'

/**
 * 节点内容定制的三种写法对比(对应源项目 playground/components/demos/Base06.vue)
 *
 * 三种写法渲染同一张卡片,卡片左上角标出「这一张是从哪个口渲染出来的」;
 * 「三种同传」这一档用来验优先级:renderNode > nodeComponent > renderContent > node.label。
 *
 * 与 Vue 用例的四处差别:
 * 1. `renderContent(node)` 只收 node,没有 Vue 传进来的 `h`(D1),直接返回 JSX;
 * 2. `#default` 作用域插槽 → `renderNode` prop(`children` 传函数等价),
 *    `node-component` → `nodeComponent`,入参形状仍是 `{ node, data }`;
 * 3. Vue 用 `markRaw(defineComponent(...))` 防 reactive 代理,React 没这个问题,
 *    但组件身份同样要稳定:`DiyCard` 必须定义在模块作用域——写在渲染函数里,
 *    每次渲染都是一个「新的组件类型」,卡片会整棵重挂载;
 * 4. 切换写法**不需要**重挂载:渲染定制虽然走 `configRef`(换回调身份不该让整树重渲染,R1),
 *    但组件对这一组 prop 做了一次全树通知,所以换了口已挂载的节点会立刻重绘,展开态保住。
 *    (这一条是文档 demo 反过来发现的库缺口:早期版本换 renderContent 视觉上毫无反应,
 *    当时只能靠 `key` 重挂载绕过。)
 */

const ROUTES = ['label', 'renderContent', 'nodeComponent', 'renderNode', 'all'] as const
type Route = (typeof ROUTES)[number]

function Card({ node, via }: { node: TreeNode; via: string }) {
  return (
    <div
      className={`flex flex-col items-start text-left ${
        node.isCurrent ? 'text-fd-primary' : 'text-fd-foreground'
      }`}
    >
      <span className="text-[10px] text-fd-muted-foreground">{via}</span>
      <span className="text-sm font-medium">{node.label}</span>
      <span className="text-xs text-fd-muted-foreground">{node.data.content}</span>
    </div>
  )
}

/** 写法一:内容区渲染函数,只有 node(源数据在 node.data,文本在 node.label) */
const renderContent: RenderContentFunction = node => <Card node={node} via="renderContent" />

/** 写法二:内容组件,props 为 { node, data } */
const DiyCard: NodeComponent = ({ node }) => <Card node={node} via="nodeComponent" />

/** 写法三:整节点渲染(对应源项目 #default 插槽),优先级最高 */
const renderNode = ({ node }: NodeComponentProps) => <Card node={node} via="renderNode" />

export function ContentModesDemo() {
  const data = useMemo(contentData, [])
  const [route, setRoute] = useState<Route>('renderContent')
  const on = (key: Route) => route === key || route === 'all'

  return (
    <div className="space-y-3">
      <div className="flex flex-wrap items-center gap-2">
        <span className="text-sm text-fd-muted-foreground">写法:</span>
        {ROUTES.map(key => (
          <button
            key={key}
            type="button"
            onClick={() => setRoute(key)}
            className={`rounded-lg border border-fd-border px-2 py-1 text-sm ${
              route === key ? 'bg-fd-accent font-medium' : 'text-fd-muted-foreground'
            }`}
          >
            {key === 'all' ? '三种同传' : key}
          </button>
        ))}
      </div>
      <p className="text-sm text-fd-muted-foreground">
        优先级 <code>renderNode</code> &gt; <code>nodeComponent</code> &gt;{' '}
        <code>renderContent</code> &gt; <code>node.label</code>。点卡片选中节点,三种写法里都读得到
        <code>node.isCurrent</code>。
      </p>
      <OkrTree
        data={data}
        direction="horizontal"
        showCollapsable
        defaultExpandAll
        renderContent={on('renderContent') ? renderContent : undefined}
        nodeComponent={on('nodeComponent') ? DiyCard : undefined}
        renderNode={on('renderNode') ? renderNode : undefined}
      />
    </div>
  )
}

renderContent

签名 (node) => ReactNode。React 不需要框架注入创建函数,因此没有 Vue 版的 h 参数(D1),直接写 JSX 即可。node 是内部 TreeNode 实例(源数据在 node.data,文本在 node.label,另有 isCurrent / expanded / leftExpanded / isLeftChild / level / childNodes / checked 等)。

function renderContent(node: TreeNode) {
  return (
    <div className={['diy', node.isCurrent && 'is-current', node.isLeftChild && 'left'].join(' ')}>
      <div>{node.data.label}</div>
      <small>{(node.data as { content?: string }).content}</small>
    </div>
  )
}

<OkrTree data={data} renderContent={renderContent} />

nodeBtnContent 用同一套入参约定(旧名仍在,新代码请用下面的 renderExpandBtn)。

nodeComponent

传一个组件,以 { node, data } 为 props 渲染:

function DeptCard({ node, data }: { node: TreeNode<Dept>; data: Dept }) {
  return (
    <b>
      {data.label}
      {node.isCurrent ? '(已选中)' : ''}
    </b>
  )
}

<OkrTree<Dept> data={depts} nodeComponent={DeptCard} />

renderNode(或 children 传函数)

对应源项目的 #default 作用域插槽。children 传函数与 renderNode 等价(children 传 JSX 节点不会被渲染,开发期会警告):

<OkrTree
  data={data}
  renderNode={({ node, data }) => (
    <>
      <b>{data.label}</b>
      {node.isCurrent ? <small>(已选中)</small> : null}
    </>
  )}
/>

展开按钮内容

优先级:showNodeNum 的折叠数字 > renderExpandBtn > nodeBtnContent;三者都缺省时渲染内置的 +/- 符号。作用域参数含 expanded / side'left' 区分 OKR 左树)/ loading(懒加载进行中)。

按钮内容:

点圆盘收起 / 展开即可看到自定义内容替掉了内置的 +/−;renderExpandBtn 拿到的 expanded 就是该侧当前状态。

xxx科技有有限公司
产品研发部
研发-前端
研发-后端
UI 设计
销售部
销售一部
销售二部
财务部

展开按钮内容

renderExpandBtn 与 showNodeNum 的优先关系。

查看源码
'use client'

import { useMemo, useState } from 'react'
import {
  OkrTree,
  type ExpandBtnScope,
  type NodeBtnContentFunction,
  type TreeNode,
} from 'react-okr-tree'
import 'react-okr-tree/style.css'
import { baseData } from './data'

/**
 * 展开圆盘内容定制(对应源项目 playground/components/demos/Base062.vue)
 *
 * 两个口 + 一条优先级:`showNodeNum` 的折叠数字 > `renderExpandBtn`(对应 #expand-btn 插槽)
 * > `nodeBtnContent`(旧名,仍在 API 表里)> 内置 CSS 画的 +/−。「两个同传」与
 * 「showNodeNum」两档就是给这条优先级用的。
 *
 * 与 Vue 用例的差别:
 * 1. 插槽 → render prop:`renderExpandBtn(scope)` 的作用域形状与 `#expand-btn` 一致
 *    (`{ node, data, expanded, side, loading }`),`side` 在 OKR 左子树才会是 'left';
 * 2. `nodeBtnContent(node)` 去掉了 Vue 的第一个参数 `h`(D1),只给 node;
 * 3. 自定义内容要包一层内置类 `org-chart-node-btn-text`:它用不透明底铺满整个圆盘,
 *    把 CSS 伪元素画的 +/− 盖掉——这属于 DOM 契约,不是可选美化;
 * 4. 圆盘是 20px 的绝对定位元素,按钮里放长文案会溢出,本例只放单字符;
 * 5. 换按钮口不需要重挂载:渲染定制这一组 prop 变更会被组件广播成一次全树重绘,
 *    已挂载节点立刻跟上,用户的展开态不受影响。
 */

const MODES = ['default', 'nodeBtnContent', 'renderExpandBtn', 'both', 'showNodeNum'] as const
type Mode = (typeof MODES)[number]

/** 旧名:与 renderContent 同一套约定,只有 node */
const nodeBtnContent: NodeBtnContentFunction = (node: TreeNode) => (
  <span className="org-chart-node-btn-text" title={node.label}>

  </span>
)

/** 新名(等价 #expand-btn 插槽):拿得到该侧的展开态与懒加载态 */
const renderExpandBtn = ({ expanded, side, loading }: ExpandBtnScope) => (
  <span className="org-chart-node-btn-text" title={side}>
    {loading ? '…' : expanded ? '−' : '+'}
  </span>
)

export function ExpandBtnDemo() {
  const data = useMemo(baseData, [])
  const [mode, setMode] = useState<Mode>('renderExpandBtn')
  const useNum = mode === 'showNodeNum'
  const useBtnContent = mode === 'nodeBtnContent' || mode === 'both' || useNum
  const useScope = mode === 'renderExpandBtn' || mode === 'both' || useNum

  return (
    <div className="space-y-3">
      <div className="flex flex-wrap items-center gap-2">
        <span className="text-sm text-fd-muted-foreground">按钮内容:</span>
        {MODES.map(key => (
          <button
            key={key}
            type="button"
            onClick={() => setMode(key)}
            className={`rounded-lg border border-fd-border px-2 py-1 text-sm ${
              mode === key ? 'bg-fd-accent font-medium' : 'text-fd-muted-foreground'
            }`}
          >
            {key === 'both' ? '两个同传' : key}
          </button>
        ))}
      </div>
      <p className="text-sm text-fd-muted-foreground">
        {useNum
          ? '树是展开的,随便收起一层就能看到圆盘里的数字:showNodeNum 优先,两个自定义口完全不会被调用。'
          : '点圆盘收起 / 展开即可看到自定义内容替掉了内置的 +/−;renderExpandBtn 拿到的 expanded 就是该侧当前状态。'}
      </p>
      <OkrTree
        data={data}
        direction="horizontal"
        showCollapsable
        defaultExpandAll
        showNodeNum={useNum}
        nodeBtnContent={useBtnContent ? nodeBtnContent : undefined}
        renderExpandBtn={useScope ? renderExpandBtn : undefined}
      />
    </div>
  )
}

空态

data 为空数组时渲染 empty(对应源项目的 #empty 插槽):

<OkrTree data={[]} empty={<span>暂无数据</span>} />

类型收窄

OkrTree<T> 本身就是泛型组件,data / leftData 与上面三种写法的渲染作用域都会带上你的数据类型,见泛型与类型推导

本页目录