Testing mobx react observer with react usecontext - React, Mobx - reactjs

New to testing mobx with React. I'm testing a a simple side navigation bar, open/closes on hamburger menu. I have set up a store for the Navbar:
export class Store {
toggle = false;
setToggle(bool){
this.toggle = bool;
};
};
decorate(Store, {
toggle: observable,
setToggle: action
});
export default createContext(new Store());
And here is the Navbar component:
export default observer(() => {
const store = useContext(Store); //SideNavStore is imported from mobx store folder
const handleOnClick = () => {
store.setToggle(false);
}
return(
<div data-testid="test">
<Drawer open={store.toggle} onClose={handleOnClick}>
<div role="presentation">
<List>
{routes.map(route => {
return <>
<ListItem button onClick={handleOnClick}>
<ListItemText primary={route.name}/>
</ListItem>
</>
})}
</List>
</div>
</Drawer>
</div>
);
});
This is App.js
export default () => {
return (
<div className="App">
<Navbar ></Navbar>
</div >
)
}
Test.js
describe('Navbar Interaction', () => {
describe('Inpsecting Navbar Contents', () => {
beforeEach(cleanup);
class Store {
sideNavToggle = true;
}
const DecoratedStore = decorate(Store,{
sideNavToggle: observable
});
const renderWithStore = (store) => {
render(
<Navbar />
);
}
it('Expect links are present', () => {
const store = new DecoratedStore();
const { getByText } = renderWithStore(store);
expect(getByText("My Dashboard")).toBeTruthy();
});
});
})
My test here fails because it can't find the text in the document, Drawer Open is set to false configured by the store.toggle. Trying to figure out how to inject the store or dummy store in the test, there are some tutorials about using Provider/Inject but that requires mobx-react and I believe they are deprecated; I would like to stick with mobx-react-lite. In renderWithStore, I'm stuck on how to pass the dummy store in the test. I could pass the store as a props but I believe that requires provider/inject which I don't want to do that if necessary. I rather import the store directly in the Navbar component using React.useContext. I don't see tests using React.useContext and Mobx Observer in the web. Has anyone encountered this type of scenario or can you provide a better approach? Also what is the best practice with using React and Mobx stores? Using React-testing-library for my tests. Your help is appreciated!

mobx-react-lite does advice users to prefer React.useContext over provider/inject.
Take a look at https://mobx-react.js.org/recipes-context
const storeContext = React.createContext<TStore | null>(null)
export const StoreProvider = ({ children }) => {
const store = useLocalStore(createStore)
return <storeContext.Provider value={store}>{children}</storeContext.Provider>
}
As for testing, if you want the component to use custom store or a mock store, you could do it by passing-in the store to StoreProvider. This how Redux users test their components.
export const StoreProvider = ({ store, children }) => {
return <storeContext.Provider value={store}>{children}</storeContext.Provider>
}

Related

Is there a reason to not import/export state in place of prop drilling or useContext in react?

Is it possible to do something like so?
MyComponent.jsx :
let myState;
let setMyState;
const MyComponent = () => {
const [myState, setMyState] = useState(35);
return (
<div> Some Jsx </div>
)
}
export myState
export setMyState
ComponentToShareState.jsx :
import myState from MyComponent.jsx
import setMyState from MyComponent.jsx
const MyComponentToShareState = () => {
const onClick = () => {
setMyState(100);
}
return (
<div>
<button onClick=(onClick)> Change my State </button>
<p>{myState}</p>
</div>)
}
Basically is there a reason that prop drilling exists rather than just exporting and importing state from component to component? Is it just for cleanliness of unidirectional flow? Would is actually break the app? Can you explain why this is not a used practice to me?

React Component with SubComponent - hot reload does not work

I have an issue with my project with a simple SubComponent. If I do any changes inside that SubComonent it is not hot reloaded. I am not sure how to fix it.
I have components defined like this:
// Component.tsx
export const Component = () => {
return <div>Component</div>;
};
Component.SubComponent = function SubComponent() {
return <div>Hello From Sub</div>;
};
export const SubComponent1 = function SubComponent1() {
return <div>Hello From Sub1</div>;
};
And usage:
// App.tsx
<Component.SubComponent />
<SubComponent1 />
If I do changes in Component.SubComponent it is not reloaded, but if I do changes in SubComponent1 it works well.
I have tested this with the clean create-react-app install and it does not work there too.
Any ideas on how to fix this or what is wrong with the code? I found quite a lot of articles about sub components on the internet.
The approach seems bad to me.
export const Component = () => {
return <div>Component</div>;
};
Component.SubComponent = function SubComponent() {
return <div>Hello From Sub</div>;
};
Export components one by one.
export const Form = () => (<></>)
export const Item = () => (<></>)
import * as Form from '...'
<Form.Form>
<Form.Item>
</Form.Item>
</Form.Form>
I see that <Form.Form /> is ugly. Another way would be:
export const Form = () => (<></>)
export const Item = () => (<></>)
import { Form, Item as FormItem } from '...'
<Form>
<Form.Item>
</Form.Item>
</Form>

How to call Parent containers function from child component in Reactjs

My application renders dynamic tiles by user and the tiles need to be re-arranged based on external configuration, StyleWrapper need to be a common container component that can be used in other projects too. Following is our UI component structure:
<StyleWrapper>
<Header />
<Content>
<PortletTiles />
</Content>
</StyleWrapper>
Given above structure, I have a function in StyleWrapper.tsx called arrangeTiles() which arranges tiles based on external configuration.
My question is how to call arrangeTiles() function from child component PortletTiles.tsx
StyleWrapper.tsx --> Parent Wrapper Container component
function StyleWrapper(props:any) {
let arrangeTiles= () => {
// Arrange Tile logic
};
return (
<div id="cnplStyleWrapper">
{props.children}
</div>
);
}
export default StyleWrapper;
PortletTiles.tsx --> Child component
function PortletTiles(props:any) {
let addNewTile= () => {
// Some logic to render tile here.. then
// How to call parent container's arrangeTiles function?
};
return (
<div>
<button onClick={addNewTile}>Add Tile</button>
</div>
);
}
export default PortletTiles;
You can create a context and pass the function or any other props as value. useContext can be used to consume the passed value from the provider.
type ContextValue = {
arrangeTiles: () => void;
};
const Context = createContext<ContextValue>({
arrangeTiles: () => {}
});
const StyleWrapper: FC = (props) => {
let arrangeTiles = () => {
// Arrange Tile logic
alert("Tiles have been arranged!");
};
return (
<Context.Provider value={{ arrangeTiles }}>
<div id="cnplStyleWrapper">{props.children}</div>
</Context.Provider>
);
};
const PortletTiles: FC = (props) => {
const { arrangeTiles } = useContext(Context);
let addNewTile = () => {
arrangeTiles();
};
return (
<div>
<button onClick={addNewTile}>Add Tile</button>
</div>
);
};
export default function App() {
return (
<StyleWrapper>
<PortletTiles />
</StyleWrapper>
);
}
If your app already uses redux, then you can move arrangeTiles to the reducer and dispatch actions from the components.
The easiest way to go about this would be to use context API. This provides you with the ability to have state that you can reference and manipulate across different components. You can reference the docs for context API here.
import { createContext } from "react";
const AppContext = createContext();
const AppProvider = (props) => {
const arrangeTiles = () => {
// place function code here
};
return (
<AppContext.Provider value={{arrangetiles}}>
{props.children}
</SiteContext.Provider>
);
};
const AppConsumer = AppContext.Consumer;
export { AppContext, AppConsumer };
export default AppProvider;
Wrap your app component in the AppProvider
In the child component, you would need to import the rearrangeTiles function and then you can use it:
import { useContext } from "react";
const { rearrangeTiles } = useContext(AppContext);

Define a functional component inside storybook preview

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 />
}}
/>

Instance returns NULL for connected component on mount in Jest

I am relatively new to react and apologies for any terms that dont fit the jargon.
I am trying to test a prototype method of a connected component which consists of a ref variable, as below:
app.js
export class Dashboard extends React.Component { // Exporting here as well
constructor(props) {
this.uploadFile = React.createRef();
this.uploadJSON = this.uploadJSON.bind(this);
}
uploadJSON () {
//Function that I am trying to test
//Conditions based on this.uploadFile
}
render() {
return (
<div className="dashboard wrapper m-padding">
<div className="dashboard-header clearfix">
<input
type="file"
ref={this.uploadFile}
webkitdirectory="true"
mozdirectory="true"
hidden
onChange={this.uploadJSON}
onClick={this.someOtherFn}
/>
</div>
<SensorStatList />
<GraphList />
</div>
);
}
const mapStateToProps = state => ({
//state
});
const mapDispatchToProps = dispatch => ({
//actions
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(Dashboard);
}
Here, SensorStatList and GraphList are functional components, also connected using redux.
After some research I have my test file to this level:
app.test.js
import { Dashboard } from '../Dashboard';
import { Provider } from 'react-redux';
import configureStore from '../../../store/store';
const store = configureStore();
export const CustomProvider = ({ children }) => {
return (
<Provider store={store}>
{children}
</Provider>
);
};
describe("Dashboard", () => {
let uploadJSONSpy = null;
function mountSetup () {
const wrapper = mount(
<CustomProvider>
<Dashboard />
</CustomProvider>
);
return {
wrapper
};
}
it("should read the file", () => {
const { wrapper } = mountSetup();
let DashboardWrapper = wrapper;
let instance = DashboardWrapper.instance();
console.log(instance.ref('uploadFile')) // TypeError: Cannot read property 'ref' of null
})
Can someone help me understand why this error
console.log(instance.ref('uploadFile'))
// TypeError: Cannot read property 'ref' of null
pops up? Also, if this approach is fine? If not, what are the better options?
wrapper is CustomProvider which has no instance, and ref is supposed to work with deprecated string refs.
In case a ref should be accessed on Dashboard, it can be:
wrapper.find(Dashboard).first().instance().uploadFile.current
In case input wrapper should be accessed, it can be:
wrapper.find('input').first()

Resources