I'm currently trying to setup redux-toolkit with Storybook. However, my selectors are returning undefined in my components when viewing them in storybook. When I run my application as a normal React application, the selectors return the appropriate state.
How do I setup Storybook with Redux so my selectors actually return the expected state from the store?
Here's my storybook story:
import { Meta, Story } from "#storybook/react"
import ContactInfo from "./ContactInfo"
import { Provider } from "react-redux"
import { store } from "../../../store/config/configureStore"
export default {
title: "Forms/ContactInfo",
component: ContactInfo,
decorators: [(story) => <Provider store={store}>{story()}</Provider>],
} as Meta
export const Template: Story<{}> = (args) => <ContactInfo {...args} />
Here's my store configuration
import { configureStore } from "#reduxjs/toolkit"
import { useDispatch } from "react-redux"
import { logger } from "../middleware/logger"
import rootReducer from "./reducer"
const store = configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
immutableCheck: false,
}).concat(logger),
})
export { store }
export type AppDispatch = typeof store.dispatch
export const useAppDispatch = () => useDispatch<AppDispatch>()
export type RootState = ReturnType<typeof rootReducer>
export interface GetState {
getState: () => RootState
}
Here's my component with a selector
import React from "react"
import { useSelector } from "react-redux"
import { useAppDispatch } from "../../../store/config/configureStore"
import {
selectContactInfo,
updateContactInfo,
} from "../../../store/contactInfo"
import Social from "../../components/social/Social"
import TextField from "../../components/textField/TextField"
export default function ContactInfo() {
const dispatch = useAppDispatch()
const contactInfo = useSelector(selectContactInfo)
console.log("printing contactInfo", contactInfo)
const handleChange = (event: any) => {
const target = event.target
const updatedContactInfo = { ...contactInfo, [target.name]: target.value }
dispatch(updateContactInfo(updatedContactInfo))
}
const handleSubmit = (event: React.SyntheticEvent) => {
console.log("User submitted contact info section: ", contactInfo, "yo")
event.preventDefault()
}
return (
<form onSubmit={handleSubmit}>
<h2>Enter Your Contact Information</h2>
<TextField
label="First Name"
value={contactInfo.firstName}
onChange={handleChange}
/>
<TextField
label="Last Name"
value={contactInfo.lastName}
onChange={handleChange}
/>
<TextField
label="Middle Initial"
value={contactInfo.middleInitial}
onChange={handleChange}
required={false}
maxLength={1}
/>
<TextField
label="Email Address"
type="email"
value={contactInfo.emailAddress}
onChange={handleChange}
/>
<Social socialLinks={contactInfo.socialLinks} />
<TextField
label="Phone Number"
type="tel"
value={contactInfo.phoneNumber}
onChange={handleChange}
/>
<button
type="button"
onClick={() => console.log("User wants to go back.")}
>
Back
</button>
<button type="submit">Next</button>
</form>
)
}
To resolve this issue, add a global decorator in your .storybook/preview.js file:
import { Provider } from "react-redux"
import { store } from "../src/store/config/configureStore"
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
}
export const decorators = [
(Story) => (
<Provider store={store}>
<Story />
</Provider>
),
]
Update 26-11-2021
It's actually a good practice to add providers (or mock of it) through the .storybook/preview.js
As showed in the storybook's screen tutorial
Related
The point is to implement a dynamic SSR component can be re-rendered by a search input.
I solved this by creating a layout.tsx file on my specific router then import children which made me dynamic render ssr component by the client component:
Layout.tsx
import React, { Suspense } from "react";
import Search from "./Search";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="layout">
<Search />
{children}
</div>
);
}
Search.tsx
"use client";
import { FormEvent, useState } from "react";
import { useRouter } from "next/navigation";
export default function Search() {
const [text, setText] = useState<string>("")
const router: any = useRouter();
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setText('')
router.push(`/definition/${text}`)
}
return (
<form onSubmit={handleSubmit} className='search'>
<input onChange={(e) => setText(e.target.value)}
value={text} type="text"
placeholder={"write to search"} />
</form>
);
} );
}
I´m looking for way to load static texts into storybook via next-translate.
My code looks like this, but it´s loading my locale files, but not writing them properly.
This is storybook preview.js:
import '../src/styles/global/global.scss';
import CssBaseline from '#material-ui/core/CssBaseline';
import { ThemeProvider } from '#material-ui/core/styles';
import theme from '../src/utils/theme';
import I18nProvider from 'next-translate/I18nProvider';
import commonCS from '../locales/cs/common.json';
export const decorators = [(Story) => themeDecorator(Story)];
const themeDecorator = (Story) => {
console.log(commonCS.homepage_title);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<I18nProvider lang={'cs-CS'} namespaces={{ commonCS }}>
<Story />
</I18nProvider>
</ThemeProvider>
);
};
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: { expanded: true },
};
And this is my storybook storie:
import React from 'react';
import HeaderContact from './HeaderContact';
import I18nProvider from 'next-translate/I18nProvider';
import useTranslation from 'next-translate/useTranslation';
import commonCS from '../../../locales/cs/common.json';
export default {
title: 'HeaderContact',
component: HeaderContact,
};
export const Basic = () => {
const { t } = useTranslation('common');
return (
<HeaderContact
link="mailto:info#numisdeal.com"
text={t('homepage_title')}
/>
);
};
My local file common.json:
{
"homepage_title": "Blog in Next.js",
"homepage_description": "This example shows a multilingual blog built in Next.js with next-translate"
}
And my translate config i18n.json
{
"locales": ["cs", "en", "de"],
"defaultLocale": "cs",
"pages": {
"*": ["common"]
}
}
I would be very glad for some help.
Thanks!
Roman
Here is the solution.
preview.js
import '../src/styles/global/global.scss';
import CssBaseline from '#material-ui/core/CssBaseline';
import { ThemeProvider } from '#material-ui/core/styles';
import theme from '../src/utils/theme';
import I18nProvider from 'next-translate/I18nProvider';
import commonCS from '../locales/cs/common.json';
export const decorators = [(Story) => themeDecorator(Story)];
const themeDecorator = (Story) => {
console.log(commonCS.homepage_title);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<I18nProvider lang={'cs'} namespaces={{ common: commonCS }}>
<Story />
</I18nProvider>
</ThemeProvider>
);
};
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: { expanded: true },
};
Storie:
import React from 'react';
import HeaderContact from './HeaderContact';
export default {
title: 'HeaderContact',
component: HeaderContact,
};
export const Basic = () => {
return <HeaderContact link="mailto:info#numisdeal.com" />;
};
Component:
import React from 'react';
import AlternateEmailIcon from '#material-ui/icons/AlternateEmail';
import useTranslation from 'next-translate/useTranslation';
import styles from './HeaderContact.module.scss';
export interface IHeaderContact {
link: string;
text?: string;
}
export default function HeaderContact(props: IHeaderContact) {
const { link, text } = props;
const { t } = useTranslation('common');
const preklad = t('homepage_title');
return (
<a href={link} className={styles.headerLink}>
<AlternateEmailIcon fontSize="small" />
<span>
{/* {text} */}
{preklad}
</span>
</a>
);
}
I'm trying to handle simple input value using react-redux and then trying to display it. I know how to display it but i have no idea how to submit input value from component to redux store. I searched web and found nothing. Can someone explain how to do this? I'm totally new to react-redux
import React from "react";
import "./App.css";
import { connect } from "react-redux";
import { useState } from "react";
import { updateValue, addValue } from "./actions/inputActions";
function App(props) {
const [value, setValue] = useState("");
const handleChange = (e) => {
setValue(e.target.value);
};
return (
<div className="App">
<form onSubmit={(value) => props.submitValue(value)}>
<input onChange={handleChange} value={value} type="text" />
<button type="submit">Add</button>
</form>
<h1>{props.value}</h1>
</div>
);
}
const mapStateToProps = (state) => {
return {
value: state.value,
};
};
const mapDispatchToProps = (dispatch) => {
return {
submitValue: (e, value) => {
e.preventDefault();
dispatch(addValue(value));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
Update your onSubmit function with the value stored in your local state, like this:
<form onSubmit={(e) => {
e.preventDefault();
props.submitValue(value)
}}>
<input onChange={handleChange} value={value} type="text" />
<button type="submit">Add</button>
</form>
And your mapDispatchToProps function like this:
const mapDispatchToProps = (dispatch) => {
return {
submitValue: (value) => {
dispatch(addValue(value));
},
};
};
I've written an action that is currently getting dispatched successfully. I can see the network request and it does return a successful response with the requested object(s), but this object is not making it back to the action. Instead its coming back in as undefined and I can't figure out why. What am I overlooking?
STORE:
import * as redux from "redux";
import thunk from "redux-thunk";
import {permitCellDesignsReducer, [other reducers her...]}
export const init = () => const reducer = redux.combineReducers({ {permitCellDesigns: permitCellDesignsReducer,...} });
const store = redux.createStore(reducer, redux.applyMiddleware(thunk));
return store;
ACTION:
export const getPermitCellDesignByFacilityId = id => {
debugger;
return dispatch => {
axios.get("/api/facilities/permits/cellDesign/" + id)
.then(res => { return res.date })
.then(cells => {
dispatch(getFacilityCellDesignsSuccess(cells));
})
}
}
const getFacilityCellDesignsSuccess = cells => {
return {
type: "GET_CELL_DESIGN_LIST",
action: cells
}
}
REDUCER:
const INITIAL_STATE = {
permitCellDesigns: {}
}
export const permitCellDesignsReducer = (state = INITIAL_STATE.permitCellDesigns, action) => {
debugger;
switch (action.type) {
case "GET_CELL_DESIGN_LIST":
return {
...state,
permitCellDesigns: action.cells
}
default:
return state
}
}
DISPATCHED ACTION:
import React from "react";
import { connect } from "react-redux";
import { Row, Col, Button } from "react-bootstrap";
import { Link } from "react-router-dom";
import FacilityHeader from '../facilities/FacilityHeader';
import * as actions from "../../actions/FacilityActions";
import * as permits from "../../actions/PermitActions";
import PermitPlanApprovalTable from './permitPlanApproval/PermitPlanApprovalTable';
import PermitCellDesignTable from './permitCellDesign/PermitCellDesignTable';
class PermitInfo extends React.Component {
componentDidMount() {
this.props.dispatch(actions.getFacilityById(this.props.id) );
this.props.dispatch(permits.getPermitPlanApprovalsByFacility (this.props.id));
this.props.dispatch(permits.getPermitCellDesignByFacilityId(this.props.id)
);
}
render() {
debugger;
const { facility, permitCellDesigns } = this.props;
if (!permitCellDesigns) {
return <div>Loading...</div>
}
return (
<div>
<FacilityHeader {...this.props} />
<Row>
<Col>
<h4>Permit/Plan Approvals</h4>
</Col>
<Col>
<Link to={{
pathname: `/facilities/permits/permitplanapproval/${this.props.id}`,
state: { facilityName: facility.facilityName }
}}><Button className="btn btn-light edit">Create</Button></Link>
</Col>
</Row>
<Row>
<Col>
<PermitPlanApprovalTable {...this.props} />
</Col>
</Row>
<Row>
<Col>
<br /><h4>Cell Design Information</h4>
</Col>
<Col>
<Link to={{
pathname: `/facilities/permits/celldesign/${this.props.id}`,
state: { facilityName: facility.facilityName }
}}><Button className="btn btn-light edit">Create</Button></Link>
</Col>
</Row>
<Row>
<Col>
<PermitCellDesignTable {...this.props} />
</Col>
</Row>
</div>
)
}
}
const mapStateToProps = state => ({
facility: state.facility.facility,
permitApprovals: state.permitApprovals.permitApprovals,
permitCellDesigns: state.permitCellDesigns.permitCellDesigns
});
export default connect(mapStateToProps)(PermitInfo);
NETWORK REQUEST RESPONSE:
ACTION RETURNED AS UNDEFINED:
All other props that were dispatched are present but not the one in question
I upgraded meteor and I started receiving deprecation warning for createContainer(). As a result, I've tried to implement withTracker however now I'm getting Component(...): A valid React element (or null) must be returned. You may have returned undefined, an array or some other invalid object.. I'm not sure what I'm missing here, can someone point out my error.
Path: App.jsx
import { Meteor } from 'meteor/meteor';
import React from 'react';
import PropTypes from 'prop-types';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { withTracker } from 'meteor/react-meteor-data';
// IsCandidate Spefic Routes
import TestContainer from '../../containers/candidate/TestContainer';
const App = appProps => (
<Router>
<ScrollToTop>
<div className="bgColor">
<NavBar {...appProps} />
<Grid className="main-page-container">
<Switch>
{/* candidate routes */}
<IsCandidate exact path="/candidate/testpage/:id" component={withTracker(TestContainer)} {...appProps} />
{/* IsPublic routes */}
<Route render={function () {
return <p>Page not found</p>;
}}
/>
</Switch>
</Grid>
</div>
</ScrollToTop>
</Router>
);
App.propTypes = {
loggingIn: PropTypes.bool,
isCandidate: PropTypes.bool
};
export default createContainer(() => {
const loggingIn = Meteor.loggingIn();
return {
loggingIn,
isCandidate: !loggingIn && !!Meteor.userId() && !!Roles.userIsInRole(Meteor.userId(), 'isCandidate'),
};
}, App);
Path: IsCandidate.jsx
import React from 'react';
import PropTypes from 'prop-types'; // ES6
import { Route, Redirect } from 'react-router-dom';
const IsCandidate = ({ loggingIn, isCandidate, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) => {
if (loggingIn) return <div />;
return isCandidate ?
(<Component loggingIn={loggingIn} isCandidate={isCandidate} {...rest} {...props} />) :
(<Redirect to="/login" />);
}}
/>
);
IsCandidate.propTypes = {
loggingIn: PropTypes.bool,
isCandidate: PropTypes.bool,
component: PropTypes.func
};
export default IsCandidate;
Path: Testcontainer.jsx
import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';
import { Test } from '../../../api/test/test';
import TestPage from '../../pages/candidate/TestPage';
export default TestContainer = withTracker(({ match }) => {
const testHandle = Meteor.subscribe('test', match.params.id);
const loadingTest = !testHandle.ready();
const testCollection = Test.findOne(match.params.id);
const testExist = !loadingTest && !!testCollection;
return {
loadingTest,
testExist,
testCollection: testExist ? testCollection : {}
};
}, TestPage);
Update
export default withTracker(() => {
const loggingIn = Meteor.loggingIn();
return {
loggingIn,
isCandidate: !loggingIn && !!Meteor.userId() && !!Roles.userIsInRole(Meteor.userId(), 'isCandidate'),
isEmployer: !loggingIn && !!Meteor.userId() && !!Roles.userIsInRole(Meteor.userId(), 'isEmployer'),
isAdmin: !loggingIn && !!Meteor.userId() && !!Roles.userIsInRole(Meteor.userId(), 'isAdmin')
};
})(App);
In App.jsx you import withTracker but use createContainer.
withTracker takes only one argument (your reactive function) and wraps your child component where createContainer took 2 arguments (function and component).
createContainer(fn, C);
withTracker(fn)(C);
EDIT
Remove the withTracker call in App.js from this line:
<IsCandidate exact path="/candidate/testpage/:id" component={withTracker(TestContainer)} {...appProps} />
so it becomes
<IsCandidate exact path="/candidate/testpage/:id" component={TestContainer} {...appProps} />
How about it?