Sharing data between React components - javascript

I have a search form in React that performs an API call and saves the data called nameservers in-state using the useState() React hook.
I'm trying to determine how I can pass this data to another component so the data can be used for rendering elsewhere on the page.
Currently, I'm just outputting the data in my return block which appears below the search form.
import React, { useState } from "react";
import { Spinner, Form, Button, Container, Col } from "react-bootstrap";
import { FaSearch } from "react-icons/fa";
const SearchForm: React.FC = () => {
const [domain, setDomain] = useState("");
const [loading, setLoading] = useState(false);
const [nameservers, setNameservers] = useState([]);
const [submitted, setSubmitted] = useState(false);
const formText = "Search for a domains";
const handleChange = (event: any) => {
setDomain(event.target.value);
};
const handleSubmit = (event: any) => {
event.preventDefault();
setLoading(true);
setSubmitted(true);
console.log(domain);
fetch(`https://dns.google.com/resolve?name=${domain}&type=NS`)
.then(results => results.json())
.then(data => {
setLoading(false);
if (data && data.Answer) {
data.Answer.sort((a: any, b: any) => a.data.localeCompare(b.data));
setNameservers(data.Answer);
}
});
};
return (
<>
<Form onSubmit={handleSubmit}>
<Form.Row>
<Col />
<Col>
<Form.Control
type="text"
placeholder={formText}
value={domain}
onChange={handleChange}
autoFocus
required
/>
</Col>
<Col>
<Button variant="primary" type="submit">
<FaSearch /> Search
</Button>
</Col>
</Form.Row>
</Form>
<hr />
{submitted &&
nameservers.map((server: any, index: number) => (
<li key={index}>{server.data}</li>
))}
{submitted && nameservers.length === 0 && <small>Error</small>}
{loading && (
<Container className="text-center">
<Spinner animation="grow" size="sm" />
<Spinner animation="grow" size="sm" />
<Spinner animation="grow" size="sm" />
</Container>
)}
</>
);
};
export default SearchForm;

There are multiple ways to share data between components. You can use one of the following options:
If the component to which you want to pass the data is a child of SearchForm component, then you can pass it as a prop.
If you are managing state via redux, you can connect components to the redux store and get the required data from the redux store in your components.
You can also use React's Context API to share common data between components that may or may not have a parent-child relationship.
If the component that needs the data from SearchForm component, is a parent component of SearchForm component, you can pass a callback function to the SearchForm component as a prop and when data is available in SearchForm component, call the callback function, received as a prop, and pass the data as an argument to that callback function.

Ciao, when I want to share data between components I use React-Redux.
Lets make an example:
Suppose that you want to share data received by server (nameservers).
At first install react-redux:
npm install react-redux
npm install redux
npm install #reduxjs/toolkit
Now we have to create the reducer and the action:
Lets say you have your component in a folder called "/components/MyComponent". Create a file called MyReducer.js.
/components/MyComponent/MyReducer.js
import { createReducer } from '#reduxjs/toolkit';
const initialState = {
nameservers: undefined,
};
const LoginReducer = createReducer(initialState, {
["SET_NAMESERVERS"]: (state, action) => {
state.nameservers= action.payload.nameservers;
},
})
export default MyReducer;
Now, on the same folder, createa file called MyAction.js
/components/MyComponent/MyAction.js
export const setNameServers = data => ({
type: "SET_NAMESERVERS",
payload: { nameservers: data }
});
Then create the store:
On your project root create a folder callled redux. Inside this create a folder called store. Then on this folder create a file called index.js.
redux/store/index.js
import { createStore, combineReducers } from "redux";
import MyReducer from '../../components/MyComponent/MyReducer';
const reducers = combineReducers({
MyReducer,
});
const store = createStore(reducers);
export default store;
Now on index.js file on root folder lets pass the store already created:
index.js
...
import { Provider } from 'react-redux';
import store from "./redux/store";
ReactDOM.render((
<Provider store={store}>
<App />
</Provider>
), document.getElementById('root') || document.createElement('div'));
We have almost done. On your component (MyComponent) you retrieve data from server. Once you have data, lets dispatch data to share into the store:
/components/MyComponent/MyComponent.js
...
import { useDispatch } from 'react-redux';
import { setNameServers } from './MyComponentAction';
const MyComponent: React.FC = () => {
const [nameservers, setNameservers] = useState([]);
const dispatch = useDispatch();
....
const handleSubmit = (event: any) => {
...
fetch(`https://dns.google.com/resolve?name=${domain}&type=NS`)
.then(results => results.json())
.then(data => {
setLoading(false);
if (data && data.Answer) {
data.Answer.sort((a: any, b: any) => a.data.localeCompare(b.data));
setNameservers(data.Answer);
dispatch(setNameServers(data.Answer)); // here the magic
}
});
};
};
Done! now you have nameservers on your react redux store and you can easly get it from another component like this:
OtherComponent.js
import { useSelector } from 'react-redux';
const OtherComponent: React.FC = () => {
const nameservers = useSelector(state => state.MyReducer.nameservers);
};
And if you log nameservers somewhere in OtherComponent you will see data retrieved in MyComponent. Awesome!

I’d do what Yousaf suggested.
SearchForm should have two props:
nameservers
handleSubmit
(This will make it easy to write a Storybook story and any tests).
Then make a wrapper for SearchForm that does the API call (handleSubmit) and sets nameservers in context. Now nameservers can be accessed elsewhere in the app.

Related

How to write cutsom redux hooks replicating what redux does fundamentally?

Just stumbled upon the question of writing custom redux hooks.
Here is a backbone of the App.js
import { Provider, useStore, useDispatch } from "./redux";
import reducers from "./reducers";
import "./styles.css";
import React} from "react";
function Hello() {
const counter = useStore(store => store.counter);
const dispatch = useDispatch();
const increment = () => dispatch({ type: "INCREMENT" });
const decrement = () => dispatch({ type: "DECREMENT" });
return (
<div>
<h3>Counter: 0</h3>
<button onClick={increment}>+ Increment</button>
<button onClick={decrement}>- Decrement</button>
</div>
);
}
// <Provider reducers={reducers}>
export default function App() {
return (
<div className="App">
<Provider reducers={reducers}>
<Hello />
</Provider>
</div>
);
}
The idea is to write implementations for useStore, useDispatch and Provider.
I got the idea that we should use context api and useReducer to have access to dispatch but then I got stuck.
I know Provider can something be like
const initialState = {};
const context = createContext(initialState);
export function Provider({ children, reducers }) {
const [state, dispatch] = useReducer(reducers, initialState);
// children need to have access to dispatch
return (
<context.Provider value={{ state, dispatch }}>{children}</context.Provider>
);
}
And similarly we could have used useContext to have access to the value passed
like
export function useStore(selector) {
const { state} = useContext(context);
return { state.selector};
}
/**
* Returns the dispatch function
*/
export function useDispatch() {
const { dispatch } = useContext(context);
return { dispatch };
}
reducers file is also a part of the sandbox link given below.
But I am stuck and cannot figure why it's not working.
Please help and explain.
Here is the work in progress sandbox
https://codesandbox.io/s/redux-forked-tlb4hb?file=/src/redux.js

Using context API (React.js) to change only a particular key in state object

I am using context API in react for managing state. For this I have created a file AppContext.js where I have created context and Provider:
import { useState, createContext } from "react";
export const AppContext = createContext();
export const AppProvider = (props) => {
const [appdata, setAppdata] = useState({
data1: "this is data1",
data2: "this is data2",
});
return (
<AppContext.Provider value={[appdata, setAppdata]}>
{props.children}
</AppContext.Provider>
);
};
I have imported this Provider in the parent component of the app i.e App.js. Also I have wrapped the <AppChild/> component in the Provider.
import AppChild from "./AppChild";
import { AppProvider } from "./AppContext";
const App = () => {
return (
<AppProvider>
<div className="App">hello</div>
<AppChild />
</AppProvider>
);
};
export default App;
Now from AppChild component, I only needed to update the data1 key of my state. For this I have created a button with a onClick through which I will be changing my state. I have used to following code in AppChild.js for this:
import { useContext } from "react";
import { AppContext } from "./AppContext";
const AppChild = () => {
const [appdata, setAppdata] = useContext(AppContext);
return (
<div>
<h3 style={{ color: "red" }}>Data for Appchild: {appdata.data1}</h3>
<button
onClick={() =>
setAppdata((prev) => {
prev.data1 = "updated data1";
return prev;
})
}
>
click to change
</button>
</div>
);
};
export default AppChild;
But when I click the button, the text inside the <h3> block is not changing. Although when I change the whole state by passing the whole object directly to setAppdata as shown below,
This way the state updates successfully. Why is the first method not working where I only wanted to change the data1 key?
You are updating state in wrong way so it is not working. This is how you should update state
<button
onClick={() =>
setAppdata((prevState) => ({
...prevState,
data1: "Your New Data"
}))
}
>
click to change
</button>

Test a react component using test and react-testing-library

I just joined a team where we use react, redux, recompose to construct components to build UI. There aren't any unit tests in the application and there isn't consistent architecture for the application. I decided to take it upon myself to add unit tests using jest and react-testing-library. I succeed with few snapshot tests but I am struggling with unit testing. I am still learning react and pretty new to redux. I would love some suggestion. I am going to share a component which renders a table with column and row. I would love a feedback.
import React, { useEffect, useState } from 'react';
import { compose } from 'recompose';
import { connect } from 'react-redux';
import { clearAll, fetchContacts } from '~/store/resources/contacts/actions';
import { isDevEnv } from '~/utils';
import Sidebar from './Sidebar';
import Table from './Table';
import Toolbar from './Toolbar';
const Contacts = ({ clearAll, fetchContacts, ...props }) => {
const [searchValue, setSearchValue] = useState('');
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [canonicalFormValues, setCanonicalFormValues] = useState({ active: true });
useEffect(() => {
fetchContacts();
return () => {
clearAll();
};
}, []);
const closeSidebar = () => {
if (isDevEnv) {
console.log('hit close function');
}
setIsSidebarOpen(false);
};
return (
<div>
<Toolbar
searchValue={searchValue}
setSearchValue={setSearchValue}
setIsSidebarOpen={setIsSidebarOpen}
/>
<Table setCanonicalFormValues={setCanonicalFormValues} />
<Sidebar
isSidebarOpen={isSidebarOpen}
closeSidebar={closeSidebar}
canonicalFormValues={canonicalFormValues}
/>
{isDevEnv && (
<div>
This is coming from the contact folder
<br />
state values:
<br />
{JSON.stringify({ searchValue })}
<br />
{JSON.stringify({ isSidebarOpen })}
<br />
{JSON.stringify({ canonicalFormValues })}
</div>
)}
</div>
);
};
const mapDispatchToProps = {
clearAll,
fetchContacts,
};
export default compose(
connect(
null,
mapDispatchToProps,
),
)(Contacts);
I generally start out with a simple "should render without crashing" test. I prefer to export and test the undecorated component, in your case Contacts.
export const Contacts = ({ clearAll, fetchContacts, ...props }) => { ...
In the test file
import React from 'react';
import { render } from '#testing-library/react';
import { Contacts } from '.';
// mock the other imported components, they should already be tested alone, right?
jest.mock('./Sidebar');
jest.mock('./Table');
jest.mock('./Toolbar');
describe('Contacts', () => {
it('should render without crashing', () = {
render(
<Contacts
// pass all the props necessary for a basic render
clearAll={jest.fn()}
fetchContacts={jest.fn()}
/>
);
});
});
At this point I run a code coverage report to see how much I have, then add more tests with varying prop values and/or using the react-testing-library's matchers to target buttons or elements to assert text is visible or trigger callbacks, etc, until I have the coverage I want.
Sometimes some of your components may rely on context provider, and in this case RTL allows you to specify wrappers. For example if your component gets decorated with react-intl for string localization, you can provide a wrapper.
export const Contacts = ({ clearAll, fetchContacts, intl }) => { ...
...
export default compose(
connect(
null,
mapDispatchToProps,
),
injectIntl,
)(Contacts);
Create a wrapper
import { IntlProvider } from 'react-intl';
const IntlWrapper = ({ children }) => (
<IntlProvider locale="en">{children}</IntlProvider>
);
const intlMock = {
...
formatMessage: message => message,
...
};
and to test, specify the wrapper in the render options argument
render(
<Contacts
// pass all the props necessary for a basic render
clearAll={jest.fn()}
fetchContacts={jest.fn()}
intl={intlMock}
/>,
{
wrapper: IntlWrapper
}
);
react-testing-library has a lot of documentation, but it is worth reading through. Hope this helps you get going.

How to update React component after changing state through redux?

I was learning React and Redux and while doing that I decided to make webpage with a button which on clicking would change the state. Below the button I wanted to display the current state in a different component. Though the button on clicking changes the state, but it is not getting reflected in the component. Here is my code:
App.js
import React from 'react'
import Name from './Name'
import {changeName} from './Action';
export default function App () {
return (
<div>
<button onClick={changeName}>Click me</button>
<Name />
</div>
)
}
Name.js
import React from 'react'
import {store} from './Store'
function Name(props) {
return (
<div>
My name is: {store.getState()}
</div>
)
}
export default Name
Store.js
import { createStore } from 'redux';
import {reducer} from './Reducer';
export const store = createStore(reducer, 'Tarun');
Action.js
import {store} from './Store';
export const changeName = () => {
if (store.getState() === "Tarun"){
store.dispatch({ type: 'name', payload: 'Subhash' });
}
else{
store.dispatch({ type: 'name', payload: 'Tarun' });
}
}
Reducer.js
export const reducer = function(state, action) {
if (action.type === 'name') {
return action.payload;
}
return state;
};
When I click the button, The text inside the Name component does not change. What is the issue?
You need to set up your reducer and initial store properly following the Redux documentation.
You're missing a Provider, which will provide your store to your application.
const store = createStore(reducer, applyMiddleware(thunk));
const rootElement = document.getElementById("root");
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
rootElement
);
Now, your store is available to your components.
Your reducer needs an initial state too and you're always supposed to return an updated copy of your state. That said, don't change the state directly, but make a copy, change it, then return that copy.
const initialState = {
name: ""
};
const reducer = function(state = initialState, action) {
if (action.type === "name") {
return { ...state, name: action.payload };
} else {
return state;
}
};
export default reducer;
You might have noticed that I added a middleware to your store, and that's because it's usually the way to go when accessing your current reducer's state in your actions. That said, I installed redux-thunk for that, so in your action, you can have something like this:
export const changeName = () => {
return (dispatch, getState) => {
if (getState().name === "Tarun") {
dispatch({ type: "name", payload: "Subhash" });
} else {
dispatch({ type: "name", payload: "Tarun" });
}
};
};
Now, with your store being provided to your app, your reducer being done and your actions being ready to go, you can connect different components to your reducer.
You use the high order component in react-redux called connect for that. For example, in your Name component, we can connect the name to be displayed to your reducer by mapping your state to the component's props:
function Name(props) {
return <div>My name is: {props.name}</div>;
}
const mapStateToProps = state => {
return {
name: state.name
};
};
export default connect(mapStateToProps)(Name);
The nice thing here is that you can also leave the first parameter in the connect high order component empty and just pass the second, which would be the dispatch functions. Well, that's what you would do in your App component, you would connect it to the changeName action.
function App(props) {
return (
<div>
<button onClick={props.changeName}>Click me</button>
<Name />
</div>
);
}
const mapDispatchToProps = dispatch => {
return {
changeName: () => dispatch(changeName())
};
};
export default connect(
null,
mapDispatchToProps
)(App);
Now, when App dispatches a changeName action, your reducer state will be updated and the other components that are connected to the reducer's state will re-render.
Summary: Try to think of your store as an empty jar of candies. Your jar starts empty, but different actions could change what's inside the jar. On top of that, different people in the house that know where the jar is can go get some candy. Translating to your problem, your app begins with an empty name and you have an action that sets up a name. The components that know where to find that name by being connected to your reducer will know when that name changes and will get the updated name.
The final code can be found here:
The only way your name component will rerender is its props or state change, or if a parent component rerenders. Making a change in redux will not automatically do this. In order to see changes to the state, you'd need to subscribe to those changes. You could do this yourself, but a far better solution is to use react-redux, which is designed for connecting react components to redux stores.
For example, you'd add a provider to your app:
import { Provider } from 'react-redux';
import { store } from './Store'
export default function App () {
return (
<Provider store={store}>
<div>
<button onClick={changeName}>Click me</button>
<Name />
</div>
</Provider>
)
}
And then you'd use connect with your Name component:
import { connect } from 'react-redux';
function Name(props) {
return (
<div>
My name is: {props.name}
</div>
)
}
const mapStateToProps = (state) => {
return { name: state };
}
export default connect(mapStateToProps)(Name)

How to use dynamic imports for redux 'actions' for bundle size optimisation

I am trying to optimise the initial page bundle size for an application. I am trying to defer loading the firebase bundle until I load a component that uses redux to make database calls.
Following is the actions file:
import { DB } from '../../firebase/initialize';
export const addText = (text, callback) => async dispatch => {
dispatch({
type: 'ADD_TEXT',
status: 'started',
});
DB.collection('texts').then(() => {
// Do something
});
};
This is loading firebase which is loading approx 100KB of code. I wanted to do load this code only after the site has completed loading.
So, I am lazy loading the component TextList that has dependency to redux action which uses firebase to get data. I was expecting this would make my actions and firebase be part of a different bundle created for TextList component and its dependency. But this is not the case.
// import react and others
import configureStore from './redux/stores/store';
import Home from './components/molecules/home/home';
ReactDOM.render(
<Provider store={configureStore()}>
<Suspense fallback={<div>Loading...</div>}>
<Home />
</Suspense>
</Provider>,
document.getElementById('root')
);
import React, { Component, lazy } from 'react';
const TextList = lazy(() => import('../../compounds/TextList/text-list'));
class Home extends Component {
render() {
return (
<div className="home-page">
<TextList />
</div>
);
}
}
export default Home;
And when Home loads, it loads redux actions at last:
import React, { Component, Suspense, lazy } from 'react';
import { connect } from 'react-redux';
import * as actions from '../../../redux/actions/actions';
class TextList extends Component {
componentDidMount() {
this.props.fetchSnippet();
}
render() {
return // template
}
}
const mapStateToProps = state => ({
...state,
});
export default connect(
mapStateToProps,
actions
)(TextList);
What approach should I follow to lazy load firebase and component using the same.
You can use a dynamic import for the firebase module in your actions file :shrug:
const getDB = async () => await import('../../firebase/initialize');
export const addText = (text, callback) => async dispatch => {
dispatch({
type: 'ADD_TEXT',
status: 'started',
});
const DB = await getDB();
DB.collection('texts').then(() => {
// Do something
});
};

Categories

Resources