How to share data from Provider to function react - javascript

I've created an NPM library that shares multiple util functions. One of which is to call our endpoints. I've included Axios in my NPM library, but I'm stuck being able to set the Axios.create instance globally.
I initially thought I could create a Provider and set a context, however, as my API function isn't within a hook, I cannot access the context. This is my first NPM library so unfamiliar with what is best practice.
// Provider.ts
export default function Provider({ children, config }: ProviderProps) {
window.config = config;
return (
<ContextConfig.Provider value={config}>{children}</ContextConfig.Provider>
);
}
^ Above, I tried using context API, setting a global variable, etc.
// api.ts
import Axios, { AxiosInstance, AxiosPromise, Cancel } from 'axios';
const axiosInstance = Axios.create(window.config);
const api = (axios: AxiosInstance) => ({
get: <T>(url: string, config: ApiRequestConfig = {}) =>
withLogger<T>(withAbort<T>(axios.get)(url, config)),
});
export default api(axiosInstance)
^ Above, tried to use the global variable window.config, however, it is undefined. Also tried converting the export to a hook to allow reading the context, however, getting errors around unsafe usage of hooks.
// index.ts
import api from './api';
import Provider from './Provider';
export { api, Provider };
The only way I can think about handling this now is using Local Storage, very much open to advise.
Cheers

You absolutely should be able to bind your variable to the window.
What I think has actually happened is that api.ts has been initiated before you set window.config, hence why it is undefined. If you converted api.ts default export to a function you'll be able to get the value of window.config on each call. I.E;
// api.ts
import Axios, { AxiosInstance, AxiosPromise, Cancel } from 'axios';
const api = (axios: AxiosInstance) => ({
get: <T>(url: string, config: ApiRequestConfig = {}) =>
withLogger<T>(withAbort<T>(axios.get)(url, config)),
});
export default () => {
const axiosInstance = Axios.create(window.config);
return api(axiosInstance)
}
This may be a little less performant as you'll be calling Axios.create on each call, however, it shouldn't be too impactful.

Do you need the config for anything except your Axios instance?
Why not just create a Provider / Context setup that handles your api object for you?
// Create a context for the api
const ApiContext = createContext({});
// Create a Provider component.
const ApiProvider = ({ config }) => {
// recreate the api every time the provided configuration changes.
const api = useMemo(() => {
// create axios instance using the provided config.
const axiosInstance = Axios.create(config);
// create API object
return {
get: <T,>(url: string, apiConfig: ApiRequestConfig = {}) => withLogger<T>(withAbort<T>(axiosInstance.get)(url, apiConfig))
};
}, [config] /* dependency array - determines when api will be recomputed */)
return (
<ApiContext.Provider value={api}>
{children}
</ApiContext.Provider>
);
};
const useApi = () => {
// retrieve configured API from context.
const api = useContext(ApiContext);
return api;
}
// Example component to show how to retrieve api for use.
const Example = () => {
// retrieve configured API from context.
const api = useContext(ApiContext);
//OR
const api = useApi();
// use api here
return (
<div>
Content goes here
</div>
)
}
// App component to show providing config for API.
const App = () => {
// Create config (should only update reference when values need to change)
const config = useMemo(() => ({
// add config here
}), []);
return (
// pass config to API Provider.
<ApiProvider config={config}>
<Example />
</ApiProvider>
)
}

Related

React call functions on renderless component

I need to have a component for handling settings, this component (called Settings) stores state using useState(), for example the primary color.
I need to create a single instance of this component and make it available to every component in the app. Luckily, I already pass down a state dict to every component (I'm very unsure if this is the correct way to achieve that btw), so I can just include this Settings constant.
My problem is that I don't know how to create the component for this purpose, so that I can call its functions and pass it to children.
Here is roughly what my Settings component looks like:
const Settings = (props) => {
const [primaryColor, setPrimaryColor] = useState("")
const getColorTheme = (): string => {
return primaryColor
}
const setColorTheme = (color: string): void => {
setPrimaryColor(color)
}
return null
}
export default Settings
Then I would like to be able to do something like this somewhere else in the app:
const App = () => {
const settings = <Settings />
return (
<div style={{ color: settings.getColorTheme() }}></div>
)
}
Bear in mind that I'm completely new to react, so my approach is probably completely wrong.
You can use a custom Higher Order Component(HOC) for this purpose, which is easier than creating a context(even thougn context is also a HOC). A HOC takes a component and returns a new component. You can send any data from your HOC to the received component.
const withSettings = (Component) => {
const [settings, setSettings] = useState({})
// ...
// ...
<Component {...props} settings={settings}/>
);
And you can use it like this:
const Component = ({ settings }) => {
...your settings UI
}
export default SettingsUI = withSettings(Component);
You can read more about HOCs in the official react documentation

How to access nuxt `$config` in Vuex state? Only access method is through store actions methods?

I have used to dotenv library to use .env file, but I have to change runtimeConfig because I realized it was easy to expose my project secret key.
In my latest project, I have used nuxt "^2.14" and mode is SPA.
So I only use "publicRuntimeConfig" in nuxt.config.ts like that.
.env
Test_BASE_URL:'https://test.org'
nuxt.config.ts
export default {
publicRuntimeConfig:{baseURL: proccess.env.Test_BASE_URL||''}
}
I can use env like that in vue file.
sample.vue
<script>
export default {
mounted(){
console.log(this.$config.baseURL)
}
}
</script>
But I couldn't use "$config" in store's state.
I tried to write that but it always return "undefied"
index.ts
export const state = (context) => ({
url:context.$config
})
I have referred the this guys solutions
and changed state's value through the actions method.
I have used SPA, so I made method like 'nuxtServerInit'as plugins.
plugins/clientInit.ts
import {Context} from "#nuxt/types";
export default function (context:Context) {
context.store.dispatch('initEnvURL',context.$config)
}
index.ts
interface State {
testURL: string
}
const state = () => ({
testURL:''
})
const mutations = {
setTestURl(state:State,config:any) {
state.testURL = config.baseURL
}
const actions = {
initEnvURL({commit},$config) {
commit('setTestURl',$config)
}
}
export default {state,mutations,actions}
I success to change state value through actions methods above,
but I don't know why "context" can't use store/state objects directly.
Does anyone know how to use $config in store/state?
or is it impossible only way to use $config through actions method like above?
That's because in Vuex, only actions actually receive the app context.
State, Mutations and Getters can't access it by design.
Your initial state should be contextless, i.e. with values that doesn't depend on the runtime execution.
Mutations are stateless, they just take a parameter and update the state. That's all. Contextful parameters should be coming from the caller.
Getters are just reactive state transformations, and should not rely on context properties, that would be messing with the Vuex module state.
So yes, what you have to do it initialise your store within the nuxtServerInit actions (or from a plugin for SPA apps):
nuxtServerInit({ store, config } ) {
store.commit('UPDATE_BASE_URL', config.baseUrl)
}
It does NOT show up through the type system even when using #nuxt/types.
Access it like this in store/index.ts or store/module.ts:
import { ActionTree, MutationTree } from 'vuex'
const actions: ActionTree<ModuleState, RootState> = {
async yourActionName({ commit }, payload): Promise<void> {
try {
let url = this.app.$config.baseURL + "/path"; // <- config is accessed here.
const res = await this.$axios.get<number>(url);
commit("mutateState", res.data);
return;
} catch (error) {
// Error handling
}
},
};
My nuxt.config.js looks like:
export default {
...
publicRuntimeConfig: {
baseURL: process.env.BASE_URL || 'http://localhost:5000/api',
}
...
};

Problem with custom ContextProvider state when using custom hook for fetching data in multiple tables in React

I have multiple tables, of small size, and I want to be able to write / read / update my components when the corresponding table has been updated by the app (we can consider it's a single user app for the moment).
I've been inspired by this question to write a custom Provider and associated hook for data fetching (and eventually posting) in my app: React useReducer async data fetch
I came up with this:
import React from "react";
import { useContext, useState, useEffect } from "react";
import axios from "axios";
const MetadataContext = React.createContext();
function MetadataContextProvider(props) {
let [metadata, setMetadata] = useState({});
async function loadMetadata(url) {
let response = await axios.get(url);
// here when I console.log the value of metadata I get {} all the time
setMetadata({ ...metadata, [url]: response.data });
}
async function postNewItem(url, payload) {
await axios.post(url, payload);
let response = await axios.get(url);
setMetadata({ ...metadata, [url]: response.data });
}
return (
<MetadataContext.Provider value={{ metadata, loadMetadata, postNewItem }}>
{props.children}
</MetadataContext.Provider>
);
}
function useMetadataTable(url) {
// this hook's goal is to allow loading data in the context provider
// when required by some component
const context = useContext(MetadataContext);
useEffect(() => {
context.loadMetadata(url);
}, []);
return [
context.metadata[url],
() => context.loadMetadata(url),
(payload) => context.postNewItem(url, payload),
];
}
function TestComponent({ url }) {
const [metadata, loadMetadata, postNewItem] = useMetadataTable(url);
// not using loadMetadata and postNewItem here
return (
<>
<p> {JSON.stringify(metadata)} </p>
</>
);
}
function App() {
return (
<MetadataContextProvider>
<TestComponent url="/api/capteur" />
<br />
<TestComponent url="/api/observation" />
</MetadataContextProvider>
);
}
export default App;
(the code should run in CRA context, both apis can be replaced with almost any API)
When I run it, a request is fired on both endpoints (/api/capteur and /api/observation), but where I'm expecting the metadata object in the MetadataContextProvider to have 2 keys: "/api/capteur" and "/api/observation", only the content of the last request made appears.
When I console.log metadata in the loadMetadata function, metadata always has the initial state hook value, that is {}.
I'm fairly new to React, I tried hard and I'm really not figuring out what's going on here. Can anyone help?
Your problem is how you update the metadata object with setMetadata.
The operation of updating the metadata object via loadMetadata in your context is done by two "instances" respectively: TestComponent #1 and TestComponent #2.
They both have access to the metadata object in your context, but they're not instantly synchronized, as useState's setter function works asynchronously.
The easy solution for your problem is called functional updates.
useState's setter does also provide a callback function, which will then use (I'm oversimplifying here) the "latest" state.
In your context provider:
async function loadMetadata(url) {
let response = await axios.get(url);
setMetadata((existingData) => ({ ...existingData, [url]: response.data }));
// instead of
// setMetadata({ ...metadata, [url]: response.data });
}
Here is a working CodeSandbox: https://codesandbox.io/s/elegant-mclean-syiol?file=/src/App.js
Look at the console to see the order of execution.
I highly recommend to fully read React hooks documentation, especially the "Hooks API Reference". There are also other problems with your code (for example missing dependencies in the useEffect hook, do you have ESLint enabled?).
If you want to have a better overview on how to use React's context I can recommend Kent C. Dodds' blog:
https://kentcdodds.com/blog/application-state-management-with-react
https://kentcdodds.com/blog/how-to-use-react-context-effectively

What's the Idiomatic way for using custom api client across react components?

My frontend application contains the ApiClient class which hides details about http communication with my server.
That's simple TypeScript class with an axios client as private field.
I faced with doubt about initializing client at root component and passing it to some children.
At this moment I initiate my client in the root component as simple js field from constructor:
constructor() {
super()
... // init state here
... // some initializations like this.handler = this.handler.bind(this)
this.apiClient = new ApiClient()
}
Some children components depend on apiClient too (e.g. login component should send request to the login endpoint, editModal component sends request for updating entity by id).
Now I'm passing apiClient as props:
<Login show={this.state.show}
handleModalClose={this.handleModalClose}
handleSuccessfulLogin={this.handleSuccessfulLogin}
httpClient={this.apiClient}
/> }
...
<EditModal
httpClient={this.apiClient}
...
/>
What is the idiomatic way for passing it to the component? Is it correct to pass the client as props?
If I understand react documentation correctly, props and state are used for rendering, and that's a bit confusing for me
If your api client doesn't depend on any props/state from the components, the best way is to initialise it separately and then just import in the file where you need to use it:
// apiClient.js
export const apiClient = new ApiClient();
// component.js
import {apiClient} from '../apiClient';
If you need to handle login/logout inside component, which sets the token inside the api client, you can add login and logout methods, which would be called after the client is initialised. Since you have only one instance of the client inside your app, these changes (login and logout) will have effect inside all the components that use the client:
// Client.js
class ApiClient {
constructor() {
// do intance init stuff if needed
this.token = null;
}
login(token) {
this.token = token;
}
logout() {
this.token = null;
}
}
// apiClient.js
export const apiClient = new ApiClient();
// component.js
import { apiClient } from '../apiClient';
const LoginPage = props => {
const handleLogin = () => {
const token = // get the token
apiClient.login(token);
}
}
// anotherComponent.js
const User = props => {
useEffect(() => {
apiClient.getUser()
}, [])
}

Write a jest test to my first component

I just finished writing my first Reactjs component and I am ready to write some tests (I used material-ui's Table and Toggle).
I read about jest and enzyme but I feel that I am still missing something.
My component looks like this (simplified):
export default class MyComponent extends Component {
constructor() {
super()
this.state = {
data: []
}
// bind methods to this
}
componentDidMount() {
this.initializeData()
}
initializeData() {
// fetch data from server and setStates
}
foo() {
// manuipulatig data
}
render() {
reutrn (
<Toggle
id="my-toggle"
...
onToggle={this.foo}
>
</Toggle>
<MyTable
id="my-table"
data={this.state.data}
...
>
</MyTable>
)
}
}
Now for the test. I want to write a test for the following scenario:
Feed initializeData with mocked data.
Toggle my-toggle
Assert data has changed (Should I assert data itself or it is better practice to assert my-table instead?)
So I started in the very beginning with:
describe('myTestCase', () => {
it('myFirstTest', () => {
const wrapper = shallow(<MyComponent/>);
}
})
I ran it, but it failed: ReferenceError: fetch is not defined
My first question is then, how do I mock initializeData to overcome the need of calling the real code that using fetch?
I followed this answer: https://stackoverflow.com/a/48082419/2022010 and came up with the following:
describe('myTestCase', () => {
it('myFirstTest', () => {
const spy = jest.spyOn(MyComponent.prototype, 'initializeData'
const wrapper = mount(<MyComponent/>);
}
})
But I am still getting the same error (I also tried it with componentDidMount instead of initializeData but it ended up the same).
Update: I was wrong. I do get a fetch is not defined error but this time it is coming from the Table component (which is a wrap for material-ui's Table). Now that I come to think about it I do have a lot of "fetches" along the way... I wonder how to take care of them then.
fetch is supported in the browser, but jest/enzyme run in a Node environment, so fetch isn't a globally available function in your test code. There are a few ways you can get around this:
1: Globally mock fetch - this is probably the simplest solution, but maybe not the cleanest.
global.fetch = jest.fn().mockResolvedValue({
json: () => /*Fake test data*/
// or mock a response with `.text()` etc if that's what
// your initializeData function uses
});
2: Abstract your fetch call into a service layer and inject that as a dependency - This will make your code more flexible (more boilerplate though), since you can hide fetch implementation behind whatever interface you choose. Then at any point in the future, if you decide to use a different fetch library, you can swap out the implementation in your service layer.
// fetchService.js
export const fetchData = (url) => {
// Simplified example, only takes 'url', doesn't
// handle errors or other params.
return fetch(url).then(res => res.json());
}
// MyComponent.js
import {fetchService} from './fetchService.js'
class MyComponent extends React.Component {
static defaultProps = {
// Pass in the imported fetchService by default. This
// way you won't have to pass it in manually in production
// but you have the option to pass it in for your tests
fetchService
}
...
initializeData() {
// Use the fetchService from props
this.props.fetchService.fetchData('some/url').then(data => {
this.setState({ data });
})
}
}
// MyComponent.jest.js
it('myFirstTest', () => {
const fetchData = jest.fn().mockResolvedValue(/*Fake test data*/);
const fetchService = { fetchData };
const wrapper = mount(<MyComponent fetchService={fetchService} />);
return Promise.resolve().then(() = {
// The mock fetch will be resolved by this point, so you can make
// expectations about your component post-initialization here
})
}

Categories

Resources