I am using next13. So I have raised storybook to version7.
However, when I start up story-book, I get the next-route error.
The cause is that I am using router in AppContext. If I comment out the router part, the storybook works.
I am making the transition to the screen according to the error content in the AppContext. (using router).
error
NextRouter was not mounted. https://nextjs.org/docs/messages/next-router-not-mounted
import * as React from 'react';
import { initialize, mswDecorator } from 'msw-storybook-addon';
import { AppContextProvider } from '../src/context/AppContextProvider';
import { NextPageWithLayout } from '../src/pages/_app.page';
import {
mockOrganization,
mockList,
} from '../src/stories/mocks/msw-handlers';
import 'style/index.css';
import 'style/utils/slider.css';
import 'tailwindcss/tailwind.css';
initialize();
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
layout: 'fullscreen',
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
msw: {
handlers: {
'/organization': mockOrganization(),
'/master': mockList(),
},
},
chromatic: {
viewports: [375, 768, 1200],
},
};
export const decorators = [
(Story: NextPageWithLayout) => {
const getLayout = Story.getLayout ?? (page => page);
return <AppContextProvider>{getLayout(<Story />)}</AppContextProvider>;
},
mswDecorator,
];
Make sure that the Router is properly initialized and mounted in your Storybook setup
import * as React from 'react';
import { Router } from 'next/router';
import { initialize, mswDecorator } from 'msw-storybook-addon';
import { AppContextProvider } from '../src/context/AppContextProvider';
import { NextPageWithLayout } from '../src/pages/_app.page';
import {
mockOrganization,
mockList,
} from '../src/stories/mocks/msw-handlers';
import 'style/index.css';
import 'style/utils/slider.css';
import 'tailwindcss/tailwind.css';
initialize();
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
layout: 'fullscreen',
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
msw: {
handlers: {
'/organization': mockOrganization(),
'/master': mockList(),
},
},
chromatic: {
viewports: [375, 768, 1200],
},
};
const RouterDecorator = (Story: NextPageWithLayout) => {
const [isRouterReady, setIsRouterReady] = React.useState(false);
React.useEffect(() => {
Router.ready(() => {
setIsRouterReady(true);
});
}, []);
const getLayout = Story.getLayout ?? (page => page);
return (
<React.Fragment>
{isRouterReady && (
<AppContextProvider>
{getLayout(<Story />)}
</AppContextProvider>
)}
</React.Fragment>
);
};
export const decorators = [RouterDecorator, mswDecorator];
Related
I'm using nextJS V9.5.5 with wp-graphql and apolloClient to get data from WordPress. Everything works fine, but when I try to return context (in the purpose of getting query) from getStaticProps() like it's described in docs, it returns an empty object.
Custom App:
import React from "react";
import getConfig from "next/config";
import LayoutOuter from "../components/LayoutOuter";
import "bootstrap/dist/css/bootstrap.css";
import { ApolloProvider } from "#apollo/client";
import { useApollo } from "../lib/apolloClient";
import { initializeApollo } from "../lib/apolloClient";
import { gql } from "#apollo/client";
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
const { DOMAIN } = publicRuntimeConfig;
function CustomApp({ pageProps, Component, props }) {
const apolloClient = useApollo(pageProps.initialApolloState);
return (
<ApolloProvider client={apolloClient}>
{console.log("_app", props)}
<LayoutOuter>
<Component {...pageProps} />
</LayoutOuter>
</ApolloProvider>
);
}
CustomApp.getInitialProps = async (ctx) => {
const apolloClient = initializeApollo();
await apolloClient.query({
query: gql`
{
// my graphql query here
}
`,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
ctx: JSON.stringify(ctx),
},
};
};
export default CustomApp;
One of the page:
import React, { Component, useEffect, useState } from "react";
import getConfig from "next/config";
import { NextSeo } from "next-seo";
const { serverRuntimeConfig, publicRuntimeConfig } = getConfig();
const { DOMAIN, SITENAME } = publicRuntimeConfig;
import { initializeApollo } from "../lib/apolloClient";
import { gql } from "#apollo/client";
import "./services.module.scss";
const Home = (props) => {
let currentPage = Object.values(props.initialApolloState.ROOT_QUERY)[1];
const {
title,
metadesc,
metaRobotsNoindex,
metaRobotsNofollow,
metaRobotsAdv,
opengraphTitle,
opengraphDescription,
opengraphImage,
twitterTitle,
twitterDescription,
twitterImage,
} = currentPage.seo;
return (
<>
{console.log("project", props)}
<NextSeo
noindex={metaRobotsNoindex}
nofollow={metaRobotsNofollow}
title={title != "" ? title : `${props.data.pagetitle} - ${SITENAME}`}
description={metadesc}
canonical={DOMAIN}
openGraph={{
url: DOMAIN,
title:
opengraphTitle != ""
? opengraphTitle
: `${props.data.pagetitle} - Garrison Collection`,
description: opengraphDescription,
images: [
{
url: opengraphImage,
width: 800,
height: 600,
alt: { SITENAME },
},
],
site_name: { SITENAME },
}}
/>
<p>works</p>
</>
);
};
export async function getStaticProps(context) {
const apolloClient = initializeApollo();
await apolloClient.query({
query: gql`
{
project(id: "ca-souls", idType: SLUG) {
seo {
canonical
metaDesc
metaKeywords
metaRobotsNofollow
metaRobotsNoindex
opengraphAuthor
opengraphDescription
opengraphModifiedTime
opengraphPublishedTime
opengraphPublisher
opengraphSiteName
opengraphTitle
opengraphType
opengraphUrl
title
twitterDescription
twitterTitle
}
}
}
`,
});
return {
props: {
initialApolloState: apolloClient.cache.extract(),
context: JSON.stringify(context) || null,
},
revalidate: 1,
};
}
export default Home;
Here is the log result:
How could I get context.query?
The context parameter includes previewData contains the preview data set by setPreviewData. This means including function, therefore, unable to serealize. Take values out from context.params.
I'm trying to use themes in Rebass, and it suggested Theme UI for theming. After following the guide on the following, I cannot get setColorMode to work in my storybook.
import useColorMode
import React from 'react'
import { ColorMode, ThemeProvider, useColorMode } from 'theme-ui'
const ThemeWrapper = (props) => {
const [colorMode, setColorMode] = useColorMode() // error
//...
}
I receive this as an error instead: [useColorMode] requires the ThemeProvider component
import useThemeUI
import { ColorMode, ThemeProvider, useThemeUI } from 'theme-ui'
const ThemeWrapper = (props) => {
const context = useThemeUI()
const { setColorMode } = context
//...
}
Later on, I have setColorMode is not a function
Examining this context using console.log, it contains the following:
{
components: Object { p: {…}, b: {…}, i: {…}, … }
emotionVersion: "10.0.27"
theme: null
}
useColorMode is nowhere to be found.
What am I doing wrong?
My current code:
.storybook/config.js
import React, { useEffect } from 'react'
import addons from '#storybook/addons';
import { addDecorator, configure } from '#storybook/react';
import { ColorMode, ThemeProvider, useThemeUI } from 'theme-ui'
import theme from '../theme'
const channel = addons.getChannel();
const ThemeWrapper = (props) => {
const context = useThemeUI()
const { setColorMode } = context
console.log(context)
const setDarkMode = isDark => setColorMode(isDark ? 'dark' : 'default')
useEffect(() => {
channel.on('DARK_MODE', setDarkMode);
return () => channel.removeListener('DARK_MODE', setDarkMode);
}, [channel, setColorMode]);
return (
<ThemeProvider theme={theme}>
<ColorMode/>
{props.children}
</ThemeProvider>
);
}
addDecorator(renderStory => <ThemeWrapper>{renderStory()}</ThemeWrapper>);
configure([
require.context('../components', true, /\.stories\.(jsx?|mdx)$/),
require.context('../stories', true, /\.stories\.(jsx?|mdx)$/)
], module);
I asked here: https://github.com/system-ui/theme-ui/issues/537 and I managed to correct my problematic code.
The error arises from the function useColorMode not being called inside a <ThemeProvider>.
I changed my config file to the following to mitigate the issue. And it fixed my problem.
import React, { useEffect } from 'react'
import addons from '#storybook/addons';
import { addDecorator, configure } from '#storybook/react';
import { ColorMode, ThemeProvider, useColorMode } from 'theme-ui'
import theme from '../theme'
const channel = addons.getChannel();
const ThemeChanger = () => {
const [colorMode, setColorMode] = useColorMode();
const setDarkMode = isDark => setColorMode(isDark ? 'dark' : 'default')
useEffect(() => {
channel.on('DARK_MODE', setDarkMode);
return () => channel.removeListener('DARK_MODE', setDarkMode);
}, [channel, setColorMode]);
return <div/>
}
const ThemeWrapper = ({ children }) => {
return (
<ThemeProvider theme={theme}>
<ThemeChanger/>
<ColorMode/>
{children}
</ThemeProvider>
);
}
addDecorator(renderStory => <ThemeWrapper>{renderStory()}</ThemeWrapper>);
configure([
require.context('../components', true, /\.stories\.(jsx?|mdx)$/),
require.context('../stories', true, /\.stories\.(jsx?|mdx)$/)
], module);
Problem:
I have created a react native application in there I am checking if not authorized I am removing the async storage and redirecting to login. To handle the Axios easily I have created an HTTP client file like this.
import axios from "axios";
import { AsyncStorage } from "react-native";
import { NavigationActions } from 'react-navigation';
// TODO: Replace this with actual JWT token from Keycloak
axios.defaults.headers.post["Content-Type"] = "application/json";
// Create axios instance for api calls
var instance = null;
export const setAuth = async () => {
const token = await AsyncStorage.getItem("jwt");
instance = axios.create({
baseURL: "",
timeout: 150000,
headers: {
Authorization: "Bearer " + token,
"Content-Type": "application/json"
}
});
instance.interceptors.response.use(
function(response) {
return response;
},
async function(error) {
console.log(error);
if (error.response.status) {
if (error.response.status === 401) {
AsyncStorage.removeItem('jwt')
AsyncStorage.removeItem("user");
NavigationActions.navigate({
routeName: 'login'
});
} else {
return error;
}
}
}
);
};
export const Get = (route, data) => {
function getData() {
return instance.get(
route,
data == null ? { data: {} } : { data: JSON.stringify(data) }
);
}
if (instance) return getData();
return setAuth().then(getData);
};
export const Post = (route, data) => {
function postData() {
return instance.post(route, JSON.stringify(data));
}
if (instance) return postData();
return setAuth().then(postData);
};
I am accessing this HTTP client file inside the redux logic function. So this is outside of the component. Problem Now I have faced is It is removing the Asyncstorage but the navigation does not seem to work correctly.
How I create My routes is this.
import React, { Component } from "react";
import { createAppContainer } from "react-navigation";
// import { createStackNavigator } from "react-navigation";
import { createBottomTabNavigator } from "react-navigation-tabs";
import { createStackNavigator } from "react-navigation-stack";
import IonIcon from "react-native-vector-icons/Ionicons";
import { Image } from "react-native";
import LoginScreen from "./components/Login/Login";
import HomeScreen from "./components/Home/Home";
import SettingsScreen from "./components/Settings/Settings";
import FinesScreen from "./components/Fines/Fines"
import ChangePassword from "./components/Changepassword/Changepassword";
const SettingsTab = createStackNavigator(
{
settings: { screen: SettingsScreen },
changePassword: { screen: ChangePassword }
},
{
initialRouteName: "settings",
headerMode: "none"
},
(navigationOptions = {
headerMode: "none"
})
);
const TabNav = createBottomTabNavigator(
{
home: {
screen: HomeScreen,
navigationOptions: {
tabBarLabel: false,
tabBarIcon: () => (
<Image source={require("../assets/invalid-name.png")} />
)
}
},
fines: {
screen: FinesScreen,
navigationOptions: {
tabBarLabel: false,
headerMode: "none",
tabBarIcon: () => (
<Image source={require("../assets/icon-service-fines.jpg")} />
)
}
},
settings: {
screen: SettingsTab,
navigationOptions: {
tabBarLabel: false,
headerMode: "none",
tabBarIcon: () => <Image source={require("../assets/settings.png")} />
}
}
},
{
tabBarPosition: "bottom",
swipeEnabled: true,
animationEnabled: true,
tabBarOptions: {
activeTintColor: "#FFFFFF",
inactiveTintColor: "#F8F8F8",
borderTopColor: "transparent",
style: {
backgroundColor: "#fffff",
borderTopColor: "transparent",
paddingTop: 0
},
indicatorStyle: {
borderBottomColor: "#87B56A",
borderBottomWidth: 2
},
tabStyle: {
justifyContent: "center"
}
}
}
);
const MainNavigator = createStackNavigator(
{
login: { screen: LoginScreen },
tab: { screen: TabNav }
},
{
initialRouteName: "login",
headerMode: "none"
},
(navigationOptions = {
headerMode: "none"
})
);
const AppContainer = createAppContainer(MainNavigator);
export default AppContainer;
An I used it in the app.js file like this.
import React from "react";
// import Login from "./src/components/Login/Login";
// import Register from "./src/components/Register/Register";
import Route from "./src/route";
import { Provider } from "react-redux";
import { createLogicMiddleware } from "redux-logic";
import { createStore, applyMiddleware, compose } from "redux";
import NavigationService from "./src/services/navigationService";
import reducers from "./src/reducers";
import services from "./src/services";
const logicMiddleware = createLogicMiddleware(services, {});
const middleware = applyMiddleware(logicMiddleware);
const composeEnhancers = compose;
const enhancer = composeEnhancers(middleware);
let store = createStore(reducers, enhancer);
export default class App extends React.Component {
render() {
return (
<Provider store={store}>
<Route
// ref={navigatorRef => {
// NavigationService.setNavigator(navigatorRef);
// }}
></Route>
</Provider>
);
}
}
Can someone help me to solve this issue? Thank you.
NavigationActions return an object that can be sent to the router using navigation.dispatch() method
You can achieve solution in multiple ways. Here I am going to explaining without Props
First you need to create a service
let _navigator;
function setNavigator(navRef) {
_navigator = navRef;
}
function navigate(navAction) {
_navigator.dispatch(
navAction
);
}
// add other navigation functions that you need and export them
export default {
navigate,
setNavigator,
};
In App container create a ref for
<AppContainer
ref={navigatorRef => {
NavigationService.setNavigator(navigatorRef);
}}
/>
Use in your http client file
import NavigationService from 'path-to-NavigationService.js';
NavigationService.navigate(NavigationActions.navigate({
routeName: 'login'
}););
expo reference
I am trying to create a unit test using React and Apollo Graphql, however I keep getting this error:
Watch Usage: Press w to show more. console.error node_modules/react-test-renderer/cjs/react-test-renderer.development.js:104
Warning: An update to ThemeHandler inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser.
in ThemeHandler (at theme-handler.spec.tsx:51)
in ApolloProvider (created by MockedProvider)
in MockedProvider (at theme-handler.spec.tsx:50)
Here is my code:
import { createMuiTheme, MuiThemeProvider } from '#material-ui/core';
import * as Sentry from '#sentry/browser';
import React, { useState } from 'react';
import { BrandTheme, useGetBrandThemeQuery } from '../../generated/graphql';
/**
* Handles the app theme. Will set the default theme or the brand theme taken from the backend.
*/
export default function ThemeHandler(props: React.PropsWithChildren<any>): React.ReactElement {
const brandId = Number(process.env.REACT_APP_BRAND);
// Default Onyo theme
const [theme, setTheme] = useState({
palette: {
primary: { main: '#f65a02' },
secondary: { main: '#520075' },
},
typography: {
fontFamily: 'Quicksand, sans-serif',
},
});
useGetBrandThemeQuery({
variables: { brandId },
skip: brandId <= 0,
onCompleted: data => {
if (
!data.brandTheme ||
!data.brandTheme.brandThemeColor ||
data.brandTheme.brandThemeColor.length === 0
) {
console.warn('Empty brand theme returned, using default');
Sentry.captureMessage(`Empty brand theme for brandId: ${brandId}`, Sentry.Severity.Warning);
} else {
const palette = parseBrandPalette(data.brandTheme as BrandTheme);
setTheme({ ...theme, palette });
console.log('Theme', theme, data.brandTheme);
}
},
});
return <MuiThemeProvider theme={createMuiTheme(theme)}>{props.children}</MuiThemeProvider>;
}
function parseBrandPalette(brandTheme: BrandTheme) {
const pallete: any = {};
for (const color of brandTheme.brandThemeColor!) {
if (color && color.key === 'primaryColor') {
pallete.primary = { main: color.value };
} else if (color && color.key === 'darkPrimaryColor') {
pallete.secondary = { main: color.value };
}
}
return pallete;
}
And my test:
import renderer from 'react-test-renderer';
import React from 'react';
import ThemeHandler from './theme-handler';
import { MockedProvider, wait } from '#apollo/react-testing';
import { GetBrandThemeDocument } from '../../generated/graphql';
import { Button } from '#material-ui/core';
const { act } = renderer;
describe('Theme Handler', () => {
const originalEnv = process.env;
beforeEach(() => {
// https://stackoverflow.com/questions/48033841/test-process-env-with-jest/48042799
jest.resetModules();
process.env = { ...originalEnv };
delete process.env.REACT_APP_BRAND;
});
afterEach(() => {
process.env = originalEnv;
});
it('should use a theme retrieved from the backend', async () => {
process.env.REACT_APP_BRAND = '39';
const mocks = [
{
request: {
query: GetBrandThemeDocument,
variables: { brandId: 39 },
},
result: {
data: {
brandTheme: {
brandThemeColor: [
{ key: 'primaryColor', value: '#182335' },
{ key: 'darkPrimaryColor', value: '#161F2F' },
],
},
},
},
},
];
let wrapper;
act(() => {
wrapper = renderer.create(
<MockedProvider mocks={mocks} addTypename={false}>
<ThemeHandler>
<Button color='primary' id='test-obj'>
Hello world!
</Button>
</ThemeHandler>
</MockedProvider>
);
});
await wait(0);
expect(wrapper).toBeTruthy();
});
});
I also tried to use Enzyme's mount instead of the React test renderer, but the result is the same.
As far as I could tell, this error is being caused because I am changing the current state using an async function and hooks. But I am not sure what could I do differently for this to work.
I solved my problem by wrapping everything on my test with act. I believe that this error was happening because part of the test was wrapped in act, but the async part wasn't, so the change was happening outside the scope of this function.
Here is the updated test, that is passing:
import React from 'react';
import ThemeHandler from './theme-handler';
import { MockedProvider, wait } from '#apollo/react-testing';
import { GetBrandThemeDocument } from '../../generated/graphql';
import { Button } from '#material-ui/core';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
describe('Theme Handler', () => {
const originalEnv = process.env;
beforeEach(() => {
// https://stackoverflow.com/questions/48033841/test-process-env-with-jest/48042799
jest.resetModules();
process.env = { ...originalEnv };
delete process.env.REACT_APP_BRAND;
});
afterEach(() => {
process.env = originalEnv;
});
it('should use a theme retrieved from the backend', async () => {
process.env.REACT_APP_BRAND = '39';
await act(async () => {
const mocks = [
{
request: {
query: GetBrandThemeDocument,
variables: { brandId: 39 },
},
result: {
data: {
brandTheme: {
brandThemeColor: [
{ key: 'primaryColor', value: '#182335' },
{ key: 'darkPrimaryColor', value: '#161F2F' },
],
},
},
},
},
];
const wrapper = mount(
<MockedProvider mocks={mocks} addTypename={false}>
<ThemeHandler>
<Button color='primary' id='test-obj'>
Hello world!
</Button>
</ThemeHandler>
</MockedProvider>
);
expect(wrapper).toBeTruthy();
await wait(0);
wrapper.update();
expect(wrapper.find('#test-obj')).toBeTruthy();
});
});
});
I'm trying to use react-docgen-typescript-loader to generate my props documentation in Storybook with my TypeScript Props, but it's not populating anything into the withInfo addon.
I'm using the TypeScript flavor of create-react-app and I'm following multiple different methods of configuring the .storybook/webpack.config.js and nothing seems to work.
Here's what my current config is:
.storybook/webpack.config.js
module.exports = ({ config, mode }) => {
config.module.rules.push({
test: /\.(ts|tsx)$/,
use: [
{
loader: require.resolve('babel-loader'),
options: {
presets: [['react-app', { flow: false, typescript: true }]],
}
},
require.resolve("react-docgen-typescript-loader"),
]
});
config.resolve.extensions.push('.ts', '.tsx');
return config;
};
.storybook/config.ts
import { configure } from '#storybook/react';
// automatically import all files ending in *.stories.js
const req = require.context('../', true, /.stories.tsx$/);
function loadStories() {
req.keys().forEach(filename => req(filename));
}
configure(loadStories, module);
stories/button.stories.tsx
import * as React from 'react';
import { storiesOf } from '#storybook/react';
import { withInfo } from '#storybook/addon-info';
import Button from '../src/components/Button';
storiesOf('Button', module)
.addDecorator(withInfo)
.add('continue', () => <Button buttonType="submit">Hello Button</Button>, { info: { inline: true } })
.add('back', () => <Button buttonType="reset">Hello Button</Button>, { info: { inline: true } });
src/components/Button.tsx
import React from 'react';
interface Props {
buttonType: Button.Type;
}
const Button: React.FunctionComponent<Props> = (props) => {
const getStyles = (buttonType: string): {color: string} => {
if (buttonType === 'reset') {
return { color: 'red' };
}
if (buttonType === 'submit') {
return { color: 'green' };
}
return { color: 'green' };
};
const { buttonType, children } = props;
return <button type={buttonType} style={getStyles(buttonType)}>{children}</button>;
};
export default Button;
There are currently no issues with this configuration, but I only see this as the info output in Storybook:
There is some difference in using named imports, this worked for me:
import React, {FC} from 'react'
...
const Button: FC<Props> = (props) => {