I have a custom modal component as functional component and in typescript. This modal component exposes api's through context providers and to access them, I'm using useContext hook.
const { openModal, closeModal } = useContext(ModalContext);
Example code on how I use this api's:
const TestComponent = () => {
const { openModal, closeModal } = useContext(ModalContext);
const modalProps = {}; //define some props
const open = () => {
openModal({...modalProps});
}
return (
<div>
<Button onClick={open}>Open Modal</Button>
</div>
)
}
And I wrap the component inside my ModalManager
<ModalManager>
<TestComponent />
</ModalManager>
This example works absolutely fine in my Modal.stories.tsx
Problem:
But this doesn't work inside my Modal.mdx. It says I cannot access react hooks outside functional component. So, I need to define a TestComponent like component to access my modal api's from context. How to define it and where to define it so that below code for preview works?
import {
Props, Preview, Meta
} from '#storybook/addon-docs/blocks';
<Meta title='Modal' />
<Preview
isExpanded
mdxSource={`
/* source of the component like in stories.tsx */
`}
>
<ModalManager><TestComponent /></ModalManager>
</Preview>
I'm not sure if this is a hack or the only way. I created the TestComponent in different tsx file and then imported it in mdx. It worked.
You may have a utility HOC to render it inside a MDX file as below
HOCComp.tsx in some Utils folder
import React, { FunctionComponent, PropsWithChildren } from 'react';
export interface HOCCompProps {
render(): React.ReactElement;
}
const HOCComp: FunctionComponent<HOCCompProps> = (props: PropsWithChildren<HOCCompProps>) => {
const { render } = props;
return render();
};
export default HOCComp;
Inside MDX File
import HOCComp from './HOC';
<HOCComp render={()=> {
function HOCImpl(){
const [count,setCount] = React.useState(180);
React.useEffect(() => {
const intId = setInterval(() => {
const newCount = count+1;
setCount(newCount);
},1000)
return () => {
clearInterval(intId);
}
})
return <Text>{count}</Text>
}
return <HOCImpl />
}}
/>
Related
I've created a custom context hook - and I'm struggling to figure out how to pass values to its provider during testing.
My hook:
import React, { createContext, useContext, useState } from 'react';
const Context = createContext({});
export const ConfigurationProvider = ({ children }) => {
// Use State to keep the values
const [configuration, setConfiguration] = useState({});
// pass the value in provider and return
return (
<Context.Provider
value={{
configuration,
setConfiguration,
}}
>
{children}
</Context.Provider>
);
};
export const useConfigurationContext = () => useContext(Context);
export const { Consumer: ConfigurationConsumer } = Context;
This is how it's used in the application:
function App() {
return (
<ConfigurationProvider>
<div className="app">
<ComponentA />
</div>
</ConfigurationProvider>
);
}
And in ComponentA:
const ComponentA = () => {
// Get configuration
const configuration = useConfigurationContext();
return (
<div>{JSON.stringify(configuration)}</div>
)
}
This all works fine - considered that I'm calling setConfiguration from another component and set an object. Now for the testing part:
import React, { Component, createContext } from 'react';
import { render, waitFor } from '#testing-library/react';
import ComponentA from 'componentA';
const config = {
propertyA: 'hello',
};
test('renders the config', async () => {
const ConfigurationContext = createContext();
const { queryByText } = render(
<ConfigurationContext.Provider value={config}>
<ComponentA />
</ConfigurationContext.Provider>
);
expect(queryByText('hello')).toBeInTheDocument();
});
This doesn't work - I'm expecting the value that I'm sending in would be rendered in the div, but the context is an empty object. What am I doing wrong?
Thanks to Carle B. Navy I got the reason why it doesn't work. For other people two wonder what the solution is I fixed it by doing the following:
In my context hook, I changed the last line to export the provider as well:
export const { Consumer: ConfigConsumer, Provider: ConfigProvider } = Context;
Then in my test case, instead of creating a new context, I import the ConfigProvider at the top, and then:
const { queryByText } = render(
<ConfigProvider value={config}>
<ComponentA />
</ConfigProvider>
);
Thanks for helping me solve this and hope this helps someone else.
I found useToast and useToastContainer, but documentation is absent and i don't understand how tu use these hooks. Can anyone provide some info about these hooks?
The toasts inherit ToastContainer’s props. Props defined on toast supersede ToastContainer’s props.
There are two ways you can use toasts in your application:
1. Define ToastContainer inside the component
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
const App = () => {
notify = () => toast("Wow so easy !");
return (
<div>
<button onClick={notify}>Notify !</button>
// You can add <ToastContainer /> in root component as well.
<ToastContainer />
</div>
);
}
2. Call toast.configure() once in your app. At the root of your app is the best place.
The library will mount a ToastContainer for you if none is mounted.
import { toast } from "react-toastify";
import 'react-toastify/dist/ReactToastify.css';
// Call it once in your app. At the root of your app is the best place
toast.configure()
const App = () => {
notify = () => toast("Wow so easy !");
return (
<button onClick={notify}>Notify !</button>
);
}
You can use either of them. I prefer the 2nd method because you only need to define toast.configure() which is quite clean way to add it.
You can add configuration as per your need like below:
toast.configure({
autoClose: 8000,
draggable: false,
//etc you get the idea
});
EDIT
If you want to use toast hooks, then you must wrap your app with the ToastProvider to have access to its context elsewhere within your app.
import { ToastProvider, useToasts } from 'react-toast-notifications'
const FormWithToasts = () => {
const { addToast } = useToasts()
const onSubmit = async value => {
const { error } = await dataPersistenceLayer(value)
if (error) {
addToast(error.message, { appearance: 'error' })
} else {
addToast('Saved Successfully', { appearance: 'success' })
}
}
return <form onSubmit={onSubmit}>...</form>
}
const App = () => (
<ToastProvider>
<FormWithToasts />
</ToastProvider>
)
Requirement: Show toast on bottom-right corner of the screen on success/error/warning/info.
I can create a toast component and place it on any component where I want to show toasts, but this requires me to put Toast component on every component where I intend to show toasts. Alternatively I can place it on the root component and somehow manage show/hide (maintain state).
What I am wondering is having something similar to following
export class NotificationService {
public notify = ({message, notificationType, timeout=5, autoClose=true, icon=''}: Notification) => {
let show: boolean = true;
let onClose = () => {//do something};
if(autoClose) {
//set timeout
}
return show ? <Toast {...{message, notificationType, onClose, icon}} /> : </>;
}
}
And call this service where ever I need to show toasts.
Would this be the correct way to achieve the required functionality?
You can use AppContext to manage the state of your toast and a hook to trigger it whenever you want.
ToastContext:
import React, { createContext, useContext, useState } from 'react';
export const ToastContext = createContext();
export const useToastState = () => {
return useContext(ToastContext);
};
export default ({ children }) => {
const [toastState, setToastState] = useState(false);
const toastContext = { toastState, setToastState };
return <ToastContext.Provider value={toastContext}>{children}</ToastContext.Provider>;
};
App:
<ToastProvider>
<App/>
<Toast show={toastState}/>
</ToastProvider>
Then anywhere within your app you can do:
import {useToastState} from 'toastContext'
const {toastState, setToastState} = useToastState();
setToastState(!toastState);
It's really basic I guess. I'm trying to add onClick callback to my script & I believe I'm missing a value that would be responsible for finding the actual item.
Main script
import React from 'react';
import { CSVLink } from 'react-csv';
import { data } from 'constants/data';
import GetAppIcon from '#material-ui/icons/GetApp';
import PropTypes from 'prop-types';
const handleClick = (callback) => {
callback(callback);
};
const DownloadData = (props) => {
const { callback } = props;
return (
<>
<CSVLink
data={data}
onClick={() => handleClick(callback)}
>
<GetAppIcon />
</CSVLink>
</>
);
};
DownloadData.propTypes = {
callback: PropTypes.func.isRequired,
};
export default DownloadData;
Storybook code
import React from 'react';
import DownloadData from 'common/components/DownloadData';
import { data } from 'constants/data';
import { action } from '#storybook/addon-actions';
export default {
title: 'DownloadData',
component: DownloadData,
};
export const download = () => (
<DownloadData
data={data}
callback={action('icon-clicked')}
/>
);
So right now with this code on click in the storybook I'd get null and I'm looking for an object.
One of the potential issues I can see is that your handleClick function is stored as it is in-memory, when you import the component. That means you're keeping reference of something that doesn't exists and expects to use it when rendering the component with the callback prop.
Each instance of a component should have its own function. To fix it, move the function declaration inside the component. Like this:
const Foo = ({ callback }) => {
// handleClick needs to be inside here
const handleClick = callback => {
console.log("clicked");
callback(callback);
};
return <div onClick={() => handleClick(callback)}>Click me!</div>;
};
Check this example.
If this doesn't fix your problem, then there is something wrong with how you're implementing Storybook. Like a missing context.
I'm trying to use React's context feature to maintain information about the user throughout the application (e.g. the user ID, which will be used in API calls by various pages). I'm aware that this is an undocumented and not recommended over Redux, but my application is pretty simple (so I don't want or need the complexity of Redux) and this seems like a common and reasonable use case for context. If there are more acceptable solutions for keeping user information globally throughout the application, though, I'm open to using a better method.
However, I'm confused about how it's to be used properly: once the user logins in through the AuthPage (a child of the ContextProvider), how do I update the context in ContextProvider so it can get to other components, like the FridgePage? (Yes, context is technically not supposed to be updated, but this is a one-time operation -- if anyone knows a way to do this when ContextProvider is initialized, that would be more ideal). Does the router get in the way?
I've copied the relevant components here.
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { HashRouter, Route, Switch } from 'react-router-dom';
import Layout from './components/Layout.jsx';
import AuthPage from './components/AuthPage.jsx';
import ContextProvider from './components/ContextProvider.jsx';
ReactDOM.render(
<ContextProvider>
<HashRouter>
<Switch>
<Route path="/login" component={AuthPage} />
<Route path="/" component={Layout} />
</Switch>
</HashRouter>
</ContextProvider>,
document.getElementById('root')
);
ContextProvider.jsx
import React from 'react';
import PropTypes from 'prop-types';
export default class ContextProvider extends React.Component {
static childContextTypes = {
user: PropTypes.object
}
// called every time the state changes
getChildContext() {
return { user: this.state.user };
}
render() {
return(
<div>
{ this.props.children }
</div>
);
}
}
AuthPage.jsx
import React from 'react';
import PropTypes from 'prop-types';
import AuthForm from './AuthForm.jsx';
import RegisterForm from './RegisterForm.jsx';
import Api from '../api.js';
export default class AuthPage extends React.Component {
static contextTypes = {
user: PropTypes.object
}
constructor(props) {
super(props);
this.updateUserContext = this.updateUserContext.bind(this);
}
updateUserContext(user) {
console.log("Updating user context");
this.context.user = user;
console.log(this.context.user);
}
render() {
return (
<div>
<AuthForm type="Login" onSubmit={Api.login} updateUser={this.updateUserContext} />
<AuthForm type="Register" onSubmit={Api.register} updateUser={this.updateUserContext} />
</div>
);
}
}
Layout.jsx
import React from 'react';
import Header from './Header.jsx';
import { Route, Switch } from 'react-router-dom';
import FridgePage from './FridgePage.jsx';
import StockPage from './StockPage.jsx';
export default class Layout extends React.Component {
render() {
return (
<div>
<Header />
<Switch>
<Route exact path="/stock" component={StockPage} />
<Route exact path="/" component={FridgePage} />
</Switch>
</div>
);
}
}
FridgePage.jsx (where I want to access this.context.user)
import React from 'react';
import PropTypes from 'prop-types';
import Api from '../api.js';
export default class FridgePage extends React.Component {
static contextTypes = {
user: PropTypes.object
}
constructor(props) {
super(props);
this.state = {
fridge: []
}
}
componentDidMount() {
debugger;
Api.getFridge(this.context.user.id)
.then((fridge) => {
this.setState({ "fridge": fridge });
})
.catch((err) => console.log(err));
}
render() {
return (
<div>
<h1>Fridge</h1>
{ this.state.fridge }
</div>
);
}
}
Simple state provider
auth module provides two functions:
withAuth - higher order component to provide authentication data to components that need it.
update - function for updating authentication status
How it works
The basic idea is that withAuth should add auth data to props that are being passed to a wrapped component.
It is done in three steps: take props that being passed to a component, add auth data, pass new props to the component.
let state = "initial state"
const withAuth = (Component) => (props) => {
const newProps = {...props, auth: state }
return <Component {...newProps} />
}
One piece that is missing is to rerender the component when the auth state changes. There are two ways to rerender a component: with setState() and forceUpdate(). Since withAuth doesn't need internal state, we will use forceUpdate() for rerendering.
We need to trigger a component rerender whenever there is a change in auth state. To do so, we need to store forceUpdate() function in a place that is accesible to update() function that will call it whenever auth state changes.
let state = "initial state"
// this stores forceUpdate() functions for all mounted components
// that need auth state
const rerenderFunctions = []
const withAuth = (Component) =>
class WithAuth extends React.Component {
componentDidMount() {
const rerenderComponent = this.forceUpdate.bind(this)
rerenderFunctions.push(rerenderComponent)
}
render() {
const newProps = {...props, auth: state }
return <Component {...newProps} />
}
}
const update = (newState) => {
state = newState
// rerender all wrapped components to reflect current auth state
rerenderFunctions.forEach((rerenderFunction) => rerenderFunction())
}
Last step is to add code that will remove rerender function when a component is going to be unmounted
let state = "initial state"
const rerenderFunctions = []
const unsubscribe = (rerenderFunciton) => {
// find position of rerenderFunction
const index = subscribers.findIndex(subscriber);
// remove it
subscribers.splice(index, 1);
}
const subscribe = (rerenderFunction) => {
// for convinience, subscribe returns a function to
// remove the rerendering when it is no longer needed
rerenderFunctions.push(rerenderFunction)
return () => unsubscribe(rerenderFunction)
}
const withAuth = (Component) =>
class WithAuth extends React.Component {
componentDidMount() {
const rerenderComponent = this.forceUpdate.bind(this)
this.unsubscribe = subscribe(rerenderComponent)
}
render() {
const newProps = {...props, auth: state }
return <Component {...newProps} />
}
componentWillUnmount() {
// remove rerenderComponent function
// since this component don't need to be rerendered
// any more
this.unsubscribe()
}
}
// auth.js
let state = "anonymous";
const subscribers = [];
const unsubscribe = subscriber => {
const index = subscribers.findIndex(subscriber);
~index && subscribers.splice(index, 1);
};
const subscribe = subscriber => {
subscribers.push(subscriber);
return () => unsubscribe(subscriber);
};
const withAuth = Component => {
return class WithAuth extends React.Component {
componentDidMount() {
this.unsubscribe = subscribe(this.forceUpdate.bind(this));
}
render() {
const newProps = { ...this.props, auth: state };
return <Component {...newProps} />;
}
componentWillUnmoount() {
this.unsubscribe();
}
};
};
const update = newState => {
state = newState;
subscribers.forEach(subscriber => subscriber());
};
// index.js
const SignInButton = <button onClick={() => update("user 1")}>Sign In</button>;
const SignOutButton = (
<button onClick={() => update("anonymous")}>Sign Out</button>
);
const AuthState = withAuth(({ auth }) => {
return (
<h2>
Auth state: {auth}
</h2>
);
});
const App = () =>
<div>
<AuthState />
{SignInButton}
{SignOutButton}
</div>;
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
playground: https://codesandbox.io/s/vKwyxYO0
here is what i did for my project:
// src/CurrentUserContext.js
import React from "react"
export const CurrentUserContext = React.createContext()
export const CurrentUserProvider = ({ children }) => {
const [currentUser, setCurrentUser] = React.useState(null)
const fetchCurrentUser = async () => {
let response = await fetch("/api/users/current")
response = await response.json()
setCurrentUser(response)
}
return (
<CurrentUserContext.Provider value={{ currentUser, fetchCurrentUser }}>
{children}
</CurrentUserContext.Provider>
)
}
export const useCurrentUser = () => React.useContext(CurrentUserContext)
and then use it like this:
setting up the provider:
// ...
import { CurrentUserProvider } from "./CurrentUserContext"
// ...
const App = () => (
<CurrentUserProvider>
...
</CurrentUserProvider>
)
export default App
and using the context in components:
...
import { useCurrentUser } from "./CurrentUserContext"
const Header = () => {
const { currentUser, fetchCurrentUser } = useCurrentUser()
React.useEffect(() => fetchCurrentUser(), [])
const logout = async (e) => {
e.preventDefault()
let response = await fetchWithCsrf("/api/session", { method: "DELETE" })
fetchCurrentUser()
}
// ...
}
...
the full source code is available on github: https://github.com/dorianmarie/emojeet
and the project can be tried live at: http://emojeet.com/
You don't update the context, you update the ContextProvider's state which will re render the children and populate the context through getChildContext; in your context you can place functions that when called update the provider's state. Make sure you also create a high order component(HOC) named something like withAuthContext that would read the context and turned it into props for a child component to consume, much like withIntl from react-intl or withRouter from react-router among many others, this will make the development of your components simpler and context independent as if at some point you decide to just move to redux you won't have to deal with context just replace the HOC with connect and mapStateToProps.
I think I wouldn't use the context to achieve this.
Even if your app is simple (and I understand you don't want to use Redux), it's a good practice to separate the model from the view.
Consider implementing a very simple Flux architecture: create a store and dispatch actions every time you have to change the model (eg. storing user). Your views just have to listen for the store event and update their DOM.
https://facebook.github.io/flux/docs/in-depth-overview.html#content
Here's a boilerplate with a tiny helper to manage Flux : https://github.com/christianalfoni/flux-react-boilerplate/blob/master/package.json