This is my component, that contains a Select child component
import { hashHistory } from 'react-router'
import { useEffect } from 'react'
import Select from 'cm/components/common/select'
import { eventTracker, events } from 'cm/common/event-tracker'
import { eventMetricsMap } from './helpers'
import { compose } from 'cm/common/utils'
const SelectLanguage = ({ handlers, languages, selectedLanguage }) => {
useEffect(() => {
hashHistory.listen(({ pathname }) => {
if (pathname.indexOf('/library')) {
handlers.onSelect(null)
}
})
}, [])
const handleSelect = ({ value }) => {
handlers.onSelect(value)
return value.toLowerCase()
}
const trackSelectLanguage = language => {
eventTracker.track(
events.switchBuildingBlickLanguage,
{
from: eventMetricsMap[ 'buildingblocks' ][ 'from' ],
language
}
)
}
return (
<Select
className="search__form--lang dropdown-selection--fixed-width"
handlers={{
onSelect: compose(
trackSelectLanguage,
handleSelect
)
}}
items={languages}
selected={selectedLanguage}
type="medium-input"
/>
)
}
I'd like to make unit test for this component using Jest. My 4 test cases that I want to cover:
renders in the document
renders items
track select open click
handles action
Unfortunately last two cases fails and can't find a reason.
Do you know how to resolve it ?
import expect from 'expect'
import { fireEvent, render } from '#testing-library/react'
import SelectLanguage from './select-language'
const dummyEvents = {
switchBuildingBlockLanguage: 'switch building blocks language'
}
jest.mock(
'cm/common/event-tracker',
() => ({
eventTracker: {
track: () => {}
},
events: {
dummyEvents
}
})
)
const languages = [
{
name: 'English',
value: 'EN',
},
{
name: 'Nederlands',
value: 'NL'
}
]
jest.mock(
'cm/components/common/select',
() => ({ items, handlers }) => {
console.log('items', items)
return (<div className="dropdown">
<div className="dropdown__menu">
{items.map(({ name }) => (
<div className="dropdown__item" onClick={handlers.onSelect}>{name}</div>
))}
</div>
</div>)
}
)
const containerPath = '.dropdown'
const itemPath = '.dropdown__item'
describe('SelectLanguage component', () => {
it('renders in the document', () => {
const { container } = render(
<SelectLanguage languages={languages}/>
)
const $container = container.querySelector(containerPath)
expect($container).toBeTruthy()
})
it('renders items', () => {
const { container } = render(
<SelectLanguage languages={languages}/>
)
const $dropdownItems = container.querySelectorAll(itemPath)
expect([ ...$dropdownItems ].length).toBe(2)
})
it('track select open click', () => {
const spyFn = jest.fn()
const { container } = render(
<SelectLanguage
selectedLanguage={{
value: 'EN'
}}
languages={languages}
handlers={{
onSelect: spyFn
}}
/>
)
const $dropdownItem = container.querySelector(itemPath)
fireEvent.click($dropdownItem)
expect(spyFn).toHaveBeenCalledWith(
dummyEvents.switchBuildingBlockLanguage,
{
from: 'building blocks',
language: 'en'
}
)
})
it('handles action', () => {
const spyFn = jest.fn()
const { container } = render(
<SelectLanguage
selectedLanguage={{
value: 'EN'
}}
languages={languages}
handlers={{
onSelect: spyFn
}}
/>
)
const $dropdownItem = container.querySelector(itemPath)
fireEvent.click($dropdownItem)
expect(spyFn).toHaveBeenCalledWith('EN')
})
})
When using events you need to wait for them to complete before running the assertions.
testing-library/react has the waitFor function:
import { fireEvent, render, waitFor } from '#testing-library/react'
// ...
it('handles action', async () => {
// ...
fireEvent.click($dropdownItem)
await waitFor(() => {
expect(spyFn).toHaveBeenCalledWith(
dummyEvents.switchBuildingBlockLanguage,
{
from: 'building blocks',
language: 'en'
}
)
});
// ...
Related
I'm experimenting with extending components in React. I'm trying to extend Handsontable using forwardRef and useImperativeHandle. First I wrap Handsontable in my own BaseTable component, adding some methods. Then I extend the BaseTable in a CustomersTable component in the same way to add even more methods and behavior. Everything seems to work well until I try to consume the CustomersTable in CustomersTableConsumer where I get some type errors. The component works just fine, it's just Typescript that isn't happy.
BaseTable:
export type BaseTableProps = {
findReplace: (v: string, rv: string) => void;
} & HotTable;
export const BaseTable = forwardRef<BaseTableProps, HotTableProps>(
(props, ref) => {
const hotRef = useRef<HotTable>(null);
const findReplace = (value: string, replaceValue: string) => {
const hot = hotRef?.current?.__hotInstance;
// ...
};
useImperativeHandle(
ref,
() =>
({
...hotRef?.current,
findReplace
} as BaseTableProps)
);
const gridSettings: Handsontable.GridSettings = {
autoColumnSize: true,
colHeaders: true,
...props.settings
};
return (
<div>
<HotTable
{...props}
ref={hotRef}
settings={gridSettings}
/>
</div>
);
}
);
CustomersTable:
export type CustomerTableProps = HotTable & {
customerTableFunc: () => void;
};
export const CustomersTable = forwardRef<CustomerTableProps, BaseTableProps>(
(props, ref) => {
const baseTableRef = useRef<BaseTableProps>(null);
const customerTableFunc = () => {
console.log("customerTableFunc");
};
useImperativeHandle(
ref,
() =>
({
...baseTableRef?.current,
customerTableFunc
} as CustomerTableProps)
);
useEffect(() => {
const y: Handsontable.ColumnSettings[] = [
{
title: "firstName",
type: "text",
wordWrap: false
},
{
title: "lastName",
type: "text",
wordWrap: false
}
];
baseTableRef?.current?.__hotInstance?.updateSettings({
columns: y
});
}, []);
return <BaseTable {...props} ref={baseTableRef} />;
}
);
CustomerTableConsumer:
export const CustomerTableConsumer = () => {
const [gridData, setGridData] = useState<string[][]>([]);
const customersTableRef = useRef<CustomerTableProps>(null);
const init = async () => {
const z = [];
z.push(["James", "Richard"]);
z.push(["Michael", "Irwin"]);
z.push(["Solomon", "Beck"]);
setGridData(z);
customersTableRef?.current?.__hotInstance?.updateData(z);
customersTableRef?.current?.customerTableFunc();
customersTableRef?.current?.findReplace("x", "y"); };
useEffect(() => {
init();
}, []);
// can't access extended props from handsontable on CustomersTable
return <CustomersTable data={gridData} ref={customersTableRef} />;
};
Here is a Codesandbox example.
How do I need to update my typings to satisfy Typescript in this scenario?
You need to specify the type of the ref for forwardRef. This type is used then later in useRef<>().
It's confusing, because HotTable is used in useRef<HotTable>(), but BaseTable can't be used the same way, as it is a functional component and because forwardRef was used in BaseTable. So, basically, for forwardRef we define a new type and then later use that in useRef<>(). Note the distinction between BaseTableRef and BaseTableProps.
Simplified example
export type MyTableRef = {
findReplace: (v: string, rv: string) => void;
};
export type MyTableProps = { width: number; height: number };
export const MyTable = forwardRef<MyTableRef, MyTableProps>(...);
// then use it in useRef
const myTableRef = useRef<MyTableRef>(null);
<MyTable width={10} height={20} ref={myTableRef} />
Final solution
https://codesandbox.io/s/hopeful-shape-h5lvw7?file=/src/BaseTable.tsx
BaseTable:
import HotTable, { HotTableProps } from "#handsontable/react";
import { registerAllModules } from "handsontable/registry";
import { forwardRef, useImperativeHandle, useRef } from "react";
import Handsontable from "handsontable";
export type BaseTableRef = {
findReplace: (v: string, rv: string) => void;
} & HotTable;
export type BaseTableProps = HotTableProps;
export const BaseTable = forwardRef<BaseTableRef, BaseTableProps>(
(props, ref) => {
registerAllModules();
const hotRef = useRef<HotTable>(null);
const findReplace = (value: string, replaceValue: string) => {
const hot = hotRef?.current?.__hotInstance;
// ...
};
useImperativeHandle(
ref,
() =>
({
...hotRef?.current,
findReplace
} as BaseTableRef)
);
const gridSettings: Handsontable.GridSettings = {
autoColumnSize: true,
colHeaders: true,
...props.settings
};
return (
<div>
<HotTable
{...props}
ref={hotRef}
settings={gridSettings}
licenseKey="non-commercial-and-evaluation"
/>
</div>
);
}
);
CustomersTable:
import Handsontable from "handsontable";
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useRef
} from "react";
import { BaseTable, BaseTableRef, BaseTableProps } from "./BaseTable";
export type CustomerTableRef = {
customerTableFunc: () => void;
} & BaseTableRef;
export type CustomerTableProps = BaseTableProps;
export const CustomersTable = forwardRef<CustomerTableRef, CustomerTableProps>(
(props, ref) => {
const baseTableRef = useRef<BaseTableRef>(null);
const customerTableFunc = () => {
console.log("customerTableFunc");
};
useImperativeHandle(
ref,
() =>
({
...baseTableRef?.current,
customerTableFunc
} as CustomerTableRef)
);
useEffect(() => {
const y: Handsontable.ColumnSettings[] = [
{
title: "firstName",
type: "text",
wordWrap: false
},
{
title: "lastName",
type: "text",
wordWrap: false
}
];
baseTableRef?.current?.__hotInstance?.updateSettings({
columns: y
});
}, []);
return <BaseTable {...props} ref={baseTableRef} />;
}
);
CustomerTableConsumer:
import { useEffect, useRef, useState } from "react";
import { CustomersTable, CustomerTableRef } from "./CustomerTable";
export const CustomerTableConsumer = () => {
const [gridData, setGridData] = useState<string[][]>([]);
const customersTableRef = useRef<CustomerTableRef>(null);
// Check console and seee that customerTableFunc from customersTable,
// findReplace from BaseTable and __hotInstance from Handsontable is available
console.log(customersTableRef?.current);
const init = async () => {
const z = [];
z.push(["James", "Richard"]);
z.push(["Michael", "Irwin"]);
z.push(["Solomon", "Beck"]);
setGridData(z);
customersTableRef?.current?.__hotInstance?.updateData(z);
customersTableRef?.current?.customerTableFunc();
};
useEffect(() => {
init();
}, []);
return <CustomersTable data={gridData} ref={customersTableRef} />;
};
In your sandbox example it's almost correct, just fix the props type for CustomersTable. I would recommend though to not use Props suffix for ref types, as it is very confusing.
https://codesandbox.io/s/unruffled-framework-1xmltj?file=/src/CustomerTable.tsx
export const CustomersTable = forwardRef<CustomerTableProps, HotTableProps>(...)
This is the component code which I am going to test.
Here I used one state hook setCheck.
import React, { SyntheticEvent, useEffect, useState } from 'react';
import { Checkbox, Grid, Header } from 'semantic-ui-react';
interface IIdentityItem {
name: string,
comment: string,
checked: boolean,
handleSetCheckState: any,
index: number
};
export default ({ name, comment, checked, handleSetCheckState, index }: IIdentityItem) => {
const [check, setCheck] = useState(true);
useEffect(() => {
setCheck(checked);
}, []);
const onChange = (e: SyntheticEvent, data: object) => {
setCheck(!check);
handleSetCheckState(index, !check);
};
return (
<Grid className='p-16 py-9 bg-white'>
<Grid.Column width='eleven' textAlign='left'>
<Header as='p' className='description'>{name}</Header>
<Header as='p' className='comment'>{comment}</Header>
</Grid.Column>
<Grid.Column width='five' verticalAlign='middle'>
<Checkbox toggle checked={check} onChange={onChange} />
</Grid.Column>
</Grid>
)
}
This is the jest unit test code.
import ChainItem from './ChainItem';
import React from 'react';
import { create, act } from 'react-test-renderer';
import { BrowserRouter } from 'react-router-dom';
import { Checkbox } from 'semantic-ui-react';
const useStateSpy = jest.spyOn(React, "useState");
describe('ChainItem', () => {
let handleSetCheckState, index;
beforeEach(() => {
handleSetCheckState = jest.fn();
index = 0;
//useStateSpy.mockReturnValueOnce([true, setCheck]);
});
it('should work', () => {
let tree;
act(() => {
tree = create(
<ChainItem
handleSetCheckState={handleSetCheckState}
index={index} />
);
});
expect(tree).toMatchSnapshot();
});
it('functions ', () => {
let setCheck = jest.fn();
useStateSpy.mockImplementationOnce(function() { return [true, setCheck] });
let tree;
act(() => {
tree = create(
<ChainItem
handleSetCheckState={handleSetCheckState}
checked={true}
index={index} />
);
});
const items = tree.root.findAllByType(Checkbox);
act(() => items[0].props.onChange({
target: {
value: false
}
}));
expect(setCheck).toHaveBeenCalled();
expect(handleSetCheckState).toHaveBeenCalledWith(
index,
false
);
});
afterAll(() => jest.resetModules());
});
But setCheck is not getting called.
What have I done wrong?
Add Unit Test for React components, but still not working with react hook testings.
I'm developing a Gutenberg Block for Wordpress and I'm getting stuck with the logic.
I need to fetch some data to populate a ComboboxControl to allow the user to select one option.
So far I could make it work but when the post is saved and the page is reloaded, the saved value of the Combobox cannot match any item from the list because it is loaded afterwards and the selected value appears blank even if it's there. If I click inside the combobox and then outside, the selected value finally shows up.
I had to put the apiFetch request outside of the Edit function to prevent endless calls to the API but I'm not sure this is good practice.
So from there I'm not sure how to improve my code or how to use hooks such as useEffect.
I need to have my data fetched and, only then, render my ComboboxControl with all the options ready.
Here is my code from the edit.js file:
import { __ } from '#wordpress/i18n';
import { useBlockProps } from '#wordpress/block-editor';
import apiFetch from '#wordpress/api-fetch';
import { ComboboxControl, SelectControl } from '#wordpress/components';
import { useState, useEffect } from '#wordpress/element';
import './editor.scss';
var options = [];
apiFetch( { path: '/wp/v2/posts/?per_page=-1' } ).then( ( posts ) => {
console.log( posts );
if ( posts.length ) {
posts.forEach( ( post ) => {
options.push( { value: post.id, label: post.title.rendered } );
} );
}
}, options );
export default function Edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps();
const [filteredOptions, setFilteredOptions] = useState( options );
const updateGroupId = ( val ) => {
setAttributes( { GroupId: parseInt( val ) } );
}
return (
<div {...blockProps}>
<ComboboxControl
label="Group"
value={attributes.GroupId}
onChange={updateGroupId}
options={filteredOptions}
onFilterValueChange={( inputValue ) =>
setFilteredOptions(
options.filter( ( option ) =>
option.label
.toLowerCase()
.indexOf( inputValue.toLowerCase() ) >= 0
)
)
}
/>
</div>
);
}
I have the same problem...
adding my code here too as it is slightly different and might help finding the solution
const { registerBlockType } = wp.blocks;
const { ComboboxControl } = wp.components;
import { useState, useEffect } from '#wordpress/element';
import apiFetch from '#wordpress/api-fetch';
registerBlockType('hm/cptSelect', {
title: 'cptSelect',
category: 'common',
icon: 'smiley',
attributes: {
post_id: {
type: 'number',
default: 0,
},
},
edit: props => {
const { attributes, setAttributes } = props;
//states
const [posts, setPosts] = useState([]);
const [filteredOptions, setFilteredOptions] = useState(posts);
//funcs
const apirequiest = async () => {
const res = await apiFetch({ path: '/wp/v2/posts' });
const options = await res.map(post=> {
return { value: post.id, label: post.title.rendered };
});
setPosts(options);
return;
};
//effects
useEffect(() => {
apirequiest();
}, []);
return (
<>
<ComboboxControl
onFilterValueChange={inputValue =>
setFilteredOptions(
posts.filter(option =>
option.label.toLowerCase().includes(inputValue.toLowerCase())
)
)
}
label='PostSelect'
value={attributes.post}
onChange={value => setAttributes({ post_id: value })}
options={filteredOptions}
/>
</>
);
},
save: props => {
const { attributes } = props;
// apiFetch({ path: `/wp/v2/posts${attributes.post_id}` }).then(res => {
// setPostsData(res[0]);
// });
return (
<>
<div>{attributes.post_id}</div>
</>
);
},
});
I've got a simple example of React Context that uses useMemo to memoize a function and all child components re-render when any are clicked. I've tried several alternatives (commented out) and none work. Please see code at stackblitz and below.
https://stackblitz.com/edit/react-yo4eth
Index.js
import React from "react";
import { render } from "react-dom";
import Hello from "./Hello";
import { GlobalProvider } from "./GlobalState";
function App() {
return (
<GlobalProvider>
<Hello />
</GlobalProvider>
);
}
render(<App />, document.getElementById("root"));
GlobalState.js
import React, {
createContext,useState,useCallback,useMemo
} from "react";
export const GlobalContext = createContext({});
export const GlobalProvider = ({ children }) => {
const [speakerList, setSpeakerList] = useState([
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true },
]);
const clickFunction = useCallback((speakerIdClicked) => {
setSpeakerList((currentState) => {
return currentState.map((rec) => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
});
});
},[]);
// const provider = useMemo(() => {
// return { clickFunction: clickFunction, speakerList: speakerList };
// }, []);
//const provider = { clickFunction: clickFunction, speakerList: speakerList };
const provider = {
clickFunction: useMemo(() => clickFunction,[]),
speakerList: speakerList,
};
return (
<GlobalContext.Provider value={provider}>{children}</GlobalContext.Provider>
);
};
Hello.js
import React, {useContext} from "react";
import Speaker from "./Speaker";
import { GlobalContext } from './GlobalState';
export default () => {
const { speakerList } = useContext(GlobalContext);
return (
<div>
{speakerList.map((rec) => {
return <Speaker speaker={rec} key={rec.id}></Speaker>;
})}
</div>
);
};
Speaker.js
import React, { useContext } from "react";
import { GlobalContext } from "./GlobalState";
export default React.memo(({ speaker }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
const { clickFunction } = useContext(GlobalContext);
return (
<>
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id}{" "}
{speaker.favorite === true ? "true" : "false"}
</button>
</>
);
});
Couple of problems in your code:
You already have memoized the clickFunction with useCallback, no need to use useMemo hook.
You are consuming the Context in Speaker component. That is what's causing the re-render of all the instances of Speaker component.
Solution:
Since you don't want to pass clickFunction as a prop from Hello component to Speaker component and want to access clickFunction directly in Speaker component, you can create a separate Context for clickFunction.
This will work because extracting clickFunction in a separate Context will allow Speaker component to not consume GlobalContext. When any button is clicked, GlobalContext will be updated, leading to the re-render of all the components consuming the GlobalContext. Since, Speaker component is consuming a separate context that is not updated, it will prevent all instances of Speaker component from re-rendering when any button is clicked.
Demo
const GlobalContext = React.createContext({});
const GlobalProvider = ({ children }) => {
const [speakerList, setSpeakerList] = React.useState([
{ name: "Crockford", id: 101, favorite: true },
{ name: "Gupta", id: 102, favorite: false },
{ name: "Ailes", id: 103, favorite: true }
]);
return (
<GlobalContext.Provider value={{ speakerList, setSpeakerList }}>
{children}
</GlobalContext.Provider>
);
};
const ClickFuncContext = React.createContext();
const ClickFuncProvider = ({ children }) => {
const { speakerList, setSpeakerList } = React.useContext(GlobalContext);
const clickFunction = React.useCallback(speakerIdClicked => {
setSpeakerList(currentState => {
return currentState.map(rec => {
if (rec.id === speakerIdClicked) {
return { ...rec, favorite: !rec.favorite };
}
return rec;
});
});
}, []);
return (
<ClickFuncContext.Provider value={clickFunction}>
{children}
</ClickFuncContext.Provider>
);
};
const Speaker = React.memo(({ speaker }) => {
console.log(`speaker ${speaker.id} ${speaker.name} ${speaker.favorite}`);
const clickFunction = React.useContext(ClickFuncContext)
return (
<div>
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id}{" "}
{speaker.favorite === true ? "true" : "false"}
</button>
</div>
);
});
function SpeakerList() {
const { speakerList } = React.useContext(GlobalContext);
return (
<div>
{speakerList.map(rec => {
return (
<Speaker speaker={rec} key={rec.id} />
);
})}
</div>
);
};
function App() {
return (
<GlobalProvider>
<ClickFuncProvider>
<SpeakerList />
</ClickFuncProvider>
</GlobalProvider>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
You can also see this demo on StackBlitz
this will not work if you access clickFuntion in children from provider because every time you updating state, provider Object will be recreated and if you wrap this object in useMemolike this:
const provider = useMemo(()=>({
clickFunction,
speakerList,
}),[speakerList])
it will be recreated each time clickFunction is fired.
instead you need to pass it as prop to the children like this:
import React, {useContext} from "react";
import Speaker from "./Speaker";
import { GlobalContext } from './GlobalState';
export default () => {
const { speakerList,clickFunction } = useContext(GlobalContext);
return (
<div>
{speakerList.map((rec) => {
return <Speaker speaker={rec} key={rec.id} clickFunction={clickFunction }></Speaker>;
})}
</div>
);
};
and for provider object no need to add useMemo to the function clickFunction it's already wrapped in useCallback equivalent to useMemo(()=>fn,[]):
const provider = {
clickFunction,
speakerList,
}
and for speaker component you don't need global context :
import React from "react";
export default React.memo(({ speaker,clickFunction }) => {
console.log("render")
return (
<>
<button
onClick={() => {
clickFunction(speaker.id);
}}
>
{speaker.name} {speaker.id}{" "}
{speaker.favorite === true ? "true" : "false"}
</button>
</>
);
});
I am building a custom Tab
import React from 'react';
import { addons, types } from '#storybook/addons';
import { AddonPanel } from '#storybook/components';
import { useParameter } from '#storybook/api';
export const ADDON_ID = 'storybook/principles';
export const PANEL_ID = `${ADDON_ID}/panel`;
export const PARAM_KEY = 'principles'; // to communicate from stories
const PanelContent = () => {
const { component: Component } = useParameter(PARAM_KEY, {});
if (!Component) {
return <p>Usage info is missing</p>;
}
return <Component />;
};
addons.register(ADDON_ID, api => {
addons.add(PANEL_ID, {
type: types.Panel,
title: 'Usage',
paramKey: PARAM_KEY,
render: ({ active, key }) => {
return (
<AddonPanel active={active} key={key}>
<PanelContent />
</AddonPanel>
);
},
});
});
& then using it in my stories like
storiesOf('Superman', module)
.addParameters({
component: Superman,
principles: {
component: <Anatomy />
},
})
.add('a story 1', () => <p>some data 1</p>)
.add('a story 2', () => <p>some data 2</p>)
The part where I try to pass in a JSX element like
principles: { component: <Anatomy /> }, // this does not work
principles: { component: 'i can pass in a string' }, // this does work
I get an error like below when I pass in a JSX element as a prop
How can I pass in a JSX element to storybook parameters?
Found a way:
regiter.js
import { deserialize } from 'react-serialize'; //<-- this allows json to jsx conversion
// ...constants definitions
...
const Explanation = () => {
const Explanations = useParameter(PARAM_KEY, null);
const { storyId } = useStorybookState();
const storyKey = storyId.split('--')?.[1];
const ExplanationContent = useMemo(() => {
if (storyKey && Explanations?.[storyKey])
return () => deserialize(JSON.parse(Explanations?.[storyKey]));
return () => <>No extra explanation provided for the selected story</>;
}, [storyKey, Explanations?.[storyKey]]);
return (
<div style={{ margin: 16 }}>
<ExplanationContent />
</div>
);
};
addons.register(ADDON_ID, () => {
addons.add(PANEL_ID, {
type: types.TAB,
title: ADDON_TITLE,
route: ({ storyId, refId }) =>
refId
? `/${ADDON_PATH}/${refId}_${storyId}`
: `/${ADDON_PATH}/${storyId}`,
match: ({ viewMode }) => viewMode === ADDON_PATH,
render: ({ active }) => (active ? <Explanation /> : null),
});
});
and when declaring the parameter:
{
parameters:{
component: serialize(<p>Hello world</p>)
}
}