I have mobx (mobx6) store in React18 application:
export class RootStore {
public usersStore ;
constructor() {
this.usersStore = new UsersStore(this);
}
}
const StoresContext = React.createContext(new RootStore());
export const useStores = () => React.useContext(StoresContext);
I have two components, one of them initializes the store first rendering and the other one using the data from store.
initialize - Component A:
const { usersStore } = useStores();
useEffect(() => {
startTransition(() => {
usersStore.load();
});
}, []);
reading the data - Component B:
useEffect(() => {
if (usersStore.data) {
doSomething();
}
}, [usersStore.data]);
I'm using Cypress10 to test Component B:
beforeEach(() => {
const store = new RootStore();
store.usersStore.data = createData();
const StoresContext = React.createContext(store);
cy.mount(<StoresContext.Provider value={store}>
<ComponentB />
</StoresContext.Provider>);
});
but when the test is running, the data of usersStore that is read in ComponentB, is always undefined.
I believe something is wrong with the context, because the index.ts creates context of new RootStore.
the question is - how can I pass specific store to cypress component test when mobx is defined this way?
I tried to stub React.createContext, but it caused error probably because this hook is used a lot, and I didn't find way to stub it by the parameter type, but by the parameter value what I don't have.
I tried to stub my custom hook useStores but it didn't help.
any other idea?
Related
I have a web component i created in lit, which takes in a function as input prop. but the function is not being triggered from the react component.
import React, { FC } from 'react';
import '#webcomponents/widgets'
declare global {
namespace JSX {
interface IntrinsicElements {
'webcomponents-widgets': WidgetProps
}
}
}
interface WidgetProps extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> {
var1: string,
successCallback: Function,
}
const App = () =>{
const onSuccessCallback = () =>{
console.log("add some logic here");
}
return(<webcomponents-widgets var1="test" successCallBack={onSuccessCallback}></webcomponents-widgets>)
}
How can i trigger the function in react component? I have tried this is vue 3 and is working as expected.
Am i missing something?
As pointed out in this answer, React does not handle function props for web components properly at this time.
While it's possible to use a ref to add the function property imperatively, I would suggest the more idiomatic way of doing things in web components is to not take a function as a prop but rather have the web component dispatch an event on "success" and the consumer to write an event handler.
So the implementation of <webcomponents-widgets>, instead of calling
this.successCallBack();
would instead do
const event = new Event('success', {bubbles: true, composed: true});
this.dispatch(event);
Then, in your React component you can add the event listener.
const App = () => {
const widgetRef = useRef();
const onSuccessCallback = () => {
console.log("add some logic here");
}
useEffect(() => {
widgetRef.current?.addEventListener('success', onSuccessCallback);
return () => {
widgetRef.current?.removeEventListener('success', onSuccessCallback);
}
}, []);
return(<webcomponents-widgets var1="test" ref={widgetRef}></webcomponents-widgets>);
}
The #lit-labs/react package let's you wrap the web component, turning it into a React component so you can do this kind of event handling declaratively.
React does not handle Web Components as well as other frameworks (but it is planned to be improved in the future).
What is happening here is that your successCallBack parameter gets converted to a string. You need to setup a ref on your web component and set successCallBack from a useEffect:
const App = () => {
const widgetRef = useRef();
const onSuccessCallback = () =>{
console.log("add some logic here");
}
useEffect(() => {
if (widgetRef.current) {
widgetRef.current.successCallBack = onSuccessCallback;
}
}, []);
return(<webcomponents-widgets var1="test" ref={widgetRef}></webcomponents-widgets>)
}
I have class A
export class A {
constructor(data){...}
...
}
And a component that imports A but shouldn't instantiate it immediately.
...
import { A } from './A'
export const Acomponent = () => {
var aInstance;
const apiObject = apiCall(); // call some api
useEffect(() => {
if(apiObject.status === "success"){
aInstance = new A(apiObject.data);
... // aInstance is still undefined here
}
}, [apiObject]);
}
Is a var the best way to store the class instance in this case? I don't think a state would be an option because class functions would directly change the state without using the setState function.
You probably want useRef here.
import { A } from './A'
export const Acomponent = () => {
const aRef = useRef()
useEffect(() => {
const apiObject = apiCall(); // call some api
if(apiObject.status === "success"){
aRef.current = new A(apiObject.data);
}
}, [apiObject]);
return <>{aRef.current.whatever}</>
}
Just know that any changes to aRef will not re-render. If that's a problem, then this is the wrong solution.
Or you could store the A construction params in state and build a new one every render. Or better memoize it so you only build a new one when it would create a different result:
const [aParams, setAParams] = useState({})
const aInstance = useMemo(() => new A(aParams), [aParams])
useEffect(() => {
if(apiObject.status === "success") {
setAParams(apiObject.data)
}
}, [])
Or you could store the instance in state, but you have to treat the class as immutable. That means that if any value in that instance should change, you need to create a new instance and save it in state. Most classes can't easily be made to work this way, so it's probably not a great idea.
I have the following code which I implemented caching for my users:
import { IUser } from "../../context/AuthContext";
export const usersCache = {};
export const fetchUserFromID = async (id: number): Promise<IUser> => {
try {
const res = await fetch("users.json");
const users = await res.json();
Object.keys(users).forEach((userKey) => {
const currentUser = users[userKey];
if (!currentUser) {
console.warn(`Found null user: ${userKey}`);
return;
}
usersCache[users[userKey].id] = currentUser;
});
const user = usersCache[id];
return user;
} catch (e) {
console.error(`Failed to fetch user from ID: ${id}`, e);
throw Error("Unable to fetch the selected user.");
}
};
As you can see, the variable userCache stores all the users.
It works fine and I can access this variable from all my components.
I decided that I want to "notify" all my components that the userCache has changed, and I had to move this logic to a react Context and consume it with useContext.
So the questions are:
How I can set the userCache context although the above code is not a react component? (it's just a typescript file I called 'UserService')?
I can't do:
export const fetchUserFromID = async (id: number): Promise<IUser> => {
const { setUserCache } = useContext(MembersContext);
...
}
React Hook "useContext" is called in function "fetchUserFromID" that is neither a React function component nor a custom React Hook function. React component names must start with an uppercase letter. (react-hooks/rules-of-hooks)eslint
Is there a reason to prefer Context over variable as above altough the data is not subject to change frequently?
Thanks.
How I can set the userCache context although the above code is not a react component? (it's just a typescript file I called 'UserService')?
You need to declare your context value somewhere in a component in order to use it.
const MembersContext = React.createContext({}); // initial value here
function App() {
const [users, setUsers] = useState({});
const fetchUserFromID = async (id: number) => {
/* ... */
// This will call `setUsers`
/* ... */
}
return (
<MembersContext.Provider value={users}>
{/* your app components, `fetchUserFromId` is passed down */}
</MembersContext.Provider>
);
}
function SomeComponent() {
const users = useContext(MembersContext);
return (/* you can use `users` here */);
}
Is there a reason to prefer Context over variable as above altough the data is not subject to change frequently?
If you need your components to update when the data changes you have to go with either :
a context
a state passed down through props
a redux state
More info here on how to use Contexts.
I'm writing tests for React using react-testing-library and jest and are having some problems figuring out how to set a preloadedState for my redux store, when another file imports the store.
I Have a function to set up my store like this
store.ts
export const history = createBrowserHistory()
export const makeStore = (initalState?: any) => configureStore({
reducer: createRootReducer(history),
preloadedState: initalState
})
export const store = makeStore()
and another js file like this
utils.ts
import store from 'state/store'
const userIsDefined = () => {
const state = store.getState()
if (state.user === undefined) return false
...
return true
}
I then have a test that looks something like this:
utils.test.tsx (the renderWithProvider is basically a render function that also wraps my component in a Render component, see: https://redux.js.org/recipes/writing-tests#connected-components)
describe("Test", () => {
it("Runs the function when user is defined", async () => {
const store = makeStore({ user: { id_token: '1' } })
const { container } = renderWithProvider(
<SomeComponent></SomeComponent>,
store
);
})
})
And the <SomeComponent> in turn calls the function in utils.ts
SomeComponent.tsx
const SomeComponent = () => {
if (userIsDefined() === false) return (<Forbidden/>)
...
}
Now.. What happens when the test is run seem to be like this.
utils.ts is read and reads the line import store from 'state/store', this creates and saves a store variable where the user has not yet been defined.
the utils.test.tsx is called and runs the code that calls const store = makeStore({ user: { id_token: '1' } }).
The renderWithProvider() renderes SomeComponent which in turn calls the userIsDefined function.
The if (state.user === undefined) returns false because state.user is still undefined, I think that's because utils.ts has imported the store as it were before I called my own makeStore({ user: { id_token: '1' } })?
The answer I want:
I want to make sure that when call makeStore() again it updates the previously imported version of store that is being used in utils.ts. Is there a way to to this without having to use useSelector() and pass the user value from the component to my utils function?
e.g I could do something like this, but I'd rather not since I have a lot more of these files and functions, and rely much on import store from 'state/store':
SomeComponent.tsx
const SomeComponent = () => {
const user = useSelector((state: IState) => state.user)
if (userIsDefined(user) === false) return (<Forbidden/>)
...
}
utils.ts
//import store from 'state/store'
const userIsDefined = (user) => {
//const state = store.getState()
if (user === undefined) return false
...
return true
}
As I said above I'd prefer not to do it this way.
(btw I can't seem to create a fiddle for this as I don't know how to do that for tests and with this use case)
Thank you for any help or a push in the right direction.
This is just uncovering a bug in your application: you are using direct access to store inside a react component, which is something you should never do. Your component will not rerender when that state changes and get out of sync.
If you really want a helper like that, make a custom hook out of it:
import store from 'state/store'
const useUserIsDefined = () => {
const user = useSelector(state => state.user)
if (user === undefined) return false
...
return true
}
That way your helper does not need direct access to store and the component will rerender correctly when that store value changes.
I'm using redux to manage my state. After making an API call, I would update my redux store, the component would receive the updated props from redux and I would handle update the state based on the props.
With class components I currently have a method that does this:
onEdit = async () => {
if(!this.props.item) {
await this.props.fetchItem();
}
this.setState({
item: this.props.item
});
}
The updated props would be used in the setState.
Here is an example of something similar with a functional component:
const Component = (item) => {
...
const onEdit = async () => {
if(!item) {
await this.props.fetchItem();
}
setState(item) // this doesn't work
};
...
}
Obviously the above doesn't work since item uses the same props as before.
I recognize that useEffect is probably the solution most people would go for, but I was just wondering if there was a similar solution to the class component method above, since the syntax is very nice.
Put the item into state instead of props, and use props as the parameter to the Component instead of item:
const Component = (props) => {
const [item, setItem] = useState();
const onEdit = () => {
if(!item) {
props.fetchItem().then(setItem).catch(handleError);
}
};
// ...
};