-
Passing props to components in Route - use function returning a function
-
Passing props to unknown children - clone and render props pattern
You can do it by writing a lambda function:
<Button onClick={() => showContent(index)} id="btn-next-accordion-child">
Next
</Button>
Some linters don't like lambdas - in such a case you can't just write showContent(index)
, as it's not a valid React syntax for onEvent function. You can create another function (showContentWrapper
) that will call the function you need to call with an argument.
const showContentWrapper = () => showContent(index);
<Button onClick={showContentWrapper} id="btn-next-accordion-child">
Next
</Button>
In ES6, if key and value are the same, you need to write them only once:
const brand = "audi"
const size = "s"
const myCar = {brand, size}
// myCar {brand: "audi", size: "s"}
const myAnotherCar = {brand: "volvo", size: "m"}
// myAnotherCar {brand: "volvo", size: "m"}
You can use this method to name keys in this Redux example:
import { combineReducers } from 'redux';
import quotesReducer from './Quotes/reducer';
const rootReducer = combineReducers({
quotes: quotesReducer,
});
Without naming the key, it would be
import { combineReducers } from 'redux';
import quotesReducer from './Quotes/reducer';
const rootReducer = combineReducers({
quotesReducer,
});
Then in store we'd have store.quoteReducer
that doesn't look as good as store.quotes
.
Say you have this function and you want to console log stuff inside:
const setConnectionTypeAction = ( connectionType: string) : ISetConnectionTypeAction => ({
payload: { connectionType },
type: QuoteActionTypes.SET_CONNECTION_TYPE,
});
You can do it by wrapping everythung into {}
and adding return
inside:
const setConnectionTypeAction = ( connectionType: string) : ISetConnectionTypeAction => {
console.log("kotek")
return ({
payload: { connectionType },
type: QuoteActionTypes.SET_CONNECTION_TYPE,
})
};
This one was also pretty straightforward. Similarily to when we were adding stuff to an arrow function, we need to wrap everything in {}
and have an explicit return
.
Without extra logic:
const Page: FunctionComponent<IPage> = ({
userName
...
}) =>
(
<div className={className}>
<TopNavigation
userName={userName}
userImage={userImage}
userNotifications={userNotifications}
/>
...
</div>
);
With extra logic:
const Page: FunctionComponent<IPage> = ({
...
}) =>
{
let amplifyUserName = Amplify.Auth.user.attributes.email;
return (
<div className={className}>
<TopNavigation
userName={amplifyUserName}
userImage={userImage}
userNotifications={userNotifications}
/>
...
);
var { cat } = {cat: "kocilla"}
console.log(cat)
// kocilla
Change the place where the long chain is (lol)
const mapStateToProps = (state: IAppState) => ({
aEndAddressNotListed: state.orderBuilder.order.locationA.a_end_address_not_listed,
aEndLocation: state.orderBuilder.order.locationA,
});
to
const mapStateToProps = ({
orderBuilder: {
order: {
locationA
}
}
}: IAppState) => ({
aEndAddressNotListed: locationA.a_end_address_not_listed,
aEndLocation: locationA,
});
Get the chaining outside, reuse in multiple places.
From:
switch (action.type) {
case OrderActionTypes.SOMETHING:
draft.order.locationA.locationData.fullAddress =
action.payload.quoteAddress.fullAddress.attributes;
draft.order.locationA.locationData.a_end_address_not_listed =
action.payload.quoteAddress.addressNotListed;
draft.order.locationA.locationData.readonly_postcode =
action.payload.quoteAddress.postcode;
draft.order.locationA.locationData.a_end_address_not_listed =
action.payload.quoteAddress.addressNotListed;
draft.order.locationA.locationData.readonly_postcode =
action.payload.quoteAddress.postcode;
break;
to:
case OrderActionTypes.SOMETHING: {
const { fullAddress, addressNotListed, postcode } = action.payload.quoteAddress;
locationA.locationData.fullAddress = fullAddress.attributes;
locationA.locationData.a_end_address_not_listed = addressNotListed;
locationA.locationData.readonly_postcode = postcode;
break;
}
export function* getWhoamiSaga() {
try {
yield put(getWhoamiStarted());
const response = yield call(getWhoamiRequest);
yield put(
getWhoamiSuccess({
email: response.data.attributes.email,
roles: response.data.attributes.roles,
})
);
} catch (error) {
yield put(getWhoamiError());
}
}
export default function* rootSaga() {
yield debounce(300, UserActionTypes.GET_WHOAMI, getWhoamiSaga);
}
yield call(getWhoamiRequest);
returns a following object: { data: { attributes: { email: 'test@me', roles: [] } } }
. We want to get email and roles from it. We can do it by response.data.attributes.email
, or with deconstruction:
export function* getWhoamiSaga() {
try {
yield put(getWhoamiStarted());
const { data: { attributes }} = yield call(getWhoamiRequest);
yield put(
getWhoamiSuccess({
email: attributes.email,
roles: attributes.roles,
})
);
} catch (error) {
yield put(getWhoamiError());
}
}
When you haave a connect
ed Redux component that needs testing - one that doesn't get its state and functions from props, but actually from mapStateToProps
and maapDispatchToProps
, it's possible to test such component by disconnecting it and passing the props manually.
In order to do it, we need to have additional export
in our component, to export it in the unconnected version.
export const Location: FunctionComponent<{}> = ({ . // exports unconnected component << NEW
...
}) => {
return (
...
)
};
const mapDispatchToProps = (dispatch: any) => {
return {
createQuote: (quote: any) => dispatch(createQuoteAction(quote)),
};
};
const mapStateToProps = (state: any) => {
return {
quoteBuilder: state.quoteBuilder,
};
};
export default connect( . // exports connected component
mapStateToProps,
mapDispatchToProps
)(StyledLocation);
In the tests we import it in {}
and use it by passing own props
import LocationConnected, { Location } from '../'; //LocationConnected - from export default
describe('mounted ConnectionCapacity', () => {
it('when saveAndContinue button is clicked, it calls createQuote', () => {
const fakeCreateQuote = jest.fn();
const fakeQuoteBuilder = {
quote: {
...
},
},
};
const mountedLocation = mount(
<Provider store={store}>
<ThemeProvider theme={theme}>
<Location
theme={theme}
quoteBuilder={fakeQuoteBuilder}
createQuote={fakeCreateQuote}
/>
</ThemeProvider>
</Provider>
);
...
Take the object below. We'd like to write a function that will be changing the values of fields in fullAddress
.
locationData: ILocationA = {
fullAddress: {
alk: '22 1243',
building_name: '2 Building Name',
building_number: '2 11',
post_town: '2 Town',
postcode: '2 FG 123',
street: '2 Street',
sub_building: '2 Sub Building',
county: 'London',
},
a_end_address_not_listed: true,
readonly_postcode: 'N1 8LN',
},
siteContact: siteContact,
If we wanted to replace any of the fields in fullAddress
, we could use a function that accepts this whole object, name of field and its new value and use a spread operator to connect it all together.
const withAddressField = (
location: ILocationA,
field: keyof IAddress,
value: string
) => ({
...location,
locationData: {
...location.locationData, // keep location.locationData the same as it was
fullAddress: {
...location.locationData.fullAddress, // keep the location.locationData.fullAddress as it was
[field]: value, // with exception of this line - replace value of the chosen field
},
},
});
Take a form for contact information which has multiple inputs. Each time one of them is changed, we'd like to call a function which will take the new (updated), complete contact information. Eg. the current contact info is:
contactInfo = { name: "Jot", surname: "Jotek", age: 28 }
. If we change the surname to Zet
, we'd like to pass contactInfo = { name: "Jot", surname: "Zet", age: 28 }
The form:
<p className="">Site Contact Information</p>
<div className={'form-row mr-0 ml-0'}>
<Column>
<TextInput
fieldName="name"
placeholder="Name"
value={siteContact.name}
onChange={e => onChangeWithUpdate('name', e.target.value)} // <--- same onChange func
/>
<TextInput
fieldName="email"
placeholder="Email address"
value={siteContact.surname}
onChange={e => onChangeWithUpdate('name', e.target.value)} // <--- same onChange func
/>
<TextInput
fieldName="phone"
placeholder="Mobile or landline phone number"
value={siteContact.age}
onChange={e => onChangeWithUpdate('name', e.target.value)} // <--- same onChange func
/>
</Column>
</div>
The form component has siteContact
prop which holds the name, surname and age values. All form inputs reuse the same logic for making the change in field: onChange={e => onChangeWithUpdate('fieldName <eg. age>', e.target.value)}
const updated = (fieldName: keyof ISiteContact, value: string): ISiteContact => ({
...siteContact, // keep the whole siteContact
[fieldName]: value, // but change this field
});
const onChangeWithUpdate = (field: keyof ISiteContact, value: string): void =>
onChange(updated(field, value));
This one is the best on examaple. Take the following interface:
interface Person {
name: string;
age: number;
location: string;
}
Let's say we would like to check if a sertain string matches with at least one of the keys in this interface. Such mechanism can be successfully used in examples like in the section above, where we want to change multiple fields, but want to be sure that the string passed as a fieldName actually match any of the field names. This is where keyof
comes handy.
type K1 = keyof Person; // "name" | "age" | "location"
We could use this logic in the following function:
let person: Person = {
name: "Justyna",
age: 28,
location: "London"
}
updatePerson(personFieldName: keyOf Person, newValue: string | number): Person => {(
...person,
[personFieldName]: newValue
)}
updatePerson(age, 29) // success, as age is one of the fields
updatePerson(favSport, "basketball") // error - favSport is not keyOf Person
Adding an exclamation mark after a variable (/function call) will ignore undefined or null types.
function simpleExample(a: number | undefined) {
const b: number = a; // COMPILATION ERROR: undefined is not assignable to number.
const c: number = a!; // OK
}
const goToInput = () => ref.current!.scrollIntoView(); //all good!
const goToInput = () => ref.current && ref.current.scrollIntoView(); // PR do this
X.getY()!.a()
!
tells the compiler that x.getY()
is not null
.
Try catch block with async action needs to be palced in async block.
I usually do it in async functions like here:
const sendInvite = async () => {
try {
await onSendInviteClick(userData.id);
setInviteStatus(InviteStatus.Success);
setInviteIndicator(true);
} catch (e) {
setInviteStatus(InviteStatus.Error);
}
};
However, if we want to do it in a function that is not async, we need an async block within this function like here
const APIQuoteSearch: FunctionComponent<Props> = ({ className }) => {
const onSubmit = (e: React.FormEvent) => { // no async notation
e.preventDefault();
const id = searchInput.trim();
if (id === '') {
return;
}
setLoading(true);
async () => {
try {
...
} catch (error) {
...
} finally {
...
}
})();
};
or here:
useEffect(() => {
if (
...
) {
setLoading(true);
(async () => {
try {
const { data } = await doRequest({
path: `...`,
});
} catch (e) {
setError(true);
} finally {
setLoading(false);
}
})();
}
}, []);
If you need access to the location (path in URL) in the tests, it cana be done by adding a route that updates a variable in the test.
See the documentation example here and other example below:
it('...', async () => {
jest.spyOn(getAPIQuote, 'default').mockResolvedValue({
id: '1',
attributes: { ...buildAPIQuoteListItem({}) },
});
let myLocation: Location;
const { getByPlaceholderText, getByText } = renderWithTheme(
<MemoryRouter>
<APIQuoteSearch />
<Route
path="*"
render={({ location }) => {
myLocation = location;
return null;
}}
/>
</MemoryRouter>
);
expect(myLocation!.pathname).toBe('/');
fireEvent.change(getByPlaceholderText(/enter a quote id/i), {
target: { value: '1234' },
});
await act(async () => {
fireEvent.click(getByText(/go/i));
});
expect(myLocation!.pathname).toBe(apiQuoteById('1234'));
});
If you're looking for a JS package to make saving files easier, check out file-saver.
import { doRequest, Methods } from '../../../Request';
import { saveAs } from 'file-saver';
export const downloadQuotes = async (payload: ...) => {
return doRequest({
method: Methods.POST,
path: `...`,
body: {
...
},
}).then((res: string) => {
const blob = new Blob([res], { type: 'data:text/csv;charset=utf-8,' });
saveAs(blob, 'file.csv');
});
};
test:
import { downloadQuotes } from '.';
import * as request from '../../../Request';
jest.mock('file-saver', () => ({ saveAs: jest.fn() }));
describe('...', () => {
afterAll(() => jest.restoreAllMocks());
afterEach(() => jest.resetAllMocks());
const doRequest = jest
.spyOn(request, 'doRequest')
.mockImplementation(jest.fn())
.mockResolvedValue('file');
it('calls doRequest', () => {
downloadQuotes({
...
});
expect(doRequest).toHaveBeenCalledWith({
method: request.Methods.POST,
path: `...`,
body: {
...
},
});
});
});
In React Router's Route
, one doesn't pass built components eg. <LandingPage />
, but rather a function - LandingPage
.
<Route
exact={true}
path="/"
component={LandingPage}
/>
Because of this, it's not possible to pass props (in this example credentials
) to LandingPage
in the way they are normally passed to a component (<LandingPage credentials={credentials}/>
).
If we want to pass props to this LandingPage
, we can do it in a separate function (in the example below: withCredentials
) that will pass the props and return a component with them.
<Route
exact={true}
path="/"
component={withCredentials(LandingPage)}
/>
The withCredentials
function that accepts a component, eg. LandingPage
and returns a function that returns a component. Because it returns a function that returns a component, instead of simply returning a component, we keep the Route
syntax that doesn't accept built component (<LandingPage/>
) as component
s.
const withAmplifyUser = (Component: FunctionComponent<any>) => () => {
const credentials = getCredentials();
return <Component credentials={credentials} />;
};
Another way to write it - returning function is more visible then:
const withAmplifyUser = (Component: FunctionComponent<any>) => {
return function() {
const credentials = exportFunctions.getCredentials();
return <Component credentials={credentials} />;
}
}
Passing props to children is easy when you know them, eg.
const aProp = "prop"
<ParentComponent>
<Child props={aProp} />
</ParentComponent>
A situation gets more complicated when you don't yet know your children, but you still have to pass them some props, eg.
<ParentComponent>
{children}
</ParentComponent>
In such situation, you can't just go:
<ParentComponent>
{children props={aProp}
</ParentComponent>
There are (at least) two ways to make it work though.
Iterate over all of the children and use cloneELement
to pass them props
.
const ParentComponent: FunctionComponent<IAccordionProps> = ({
children,
}) => {
const [activeIndex, setActiveIndex] = useState(0);
const childrenWithProps = React.Children.map(children, (child, index) => {
return React.cloneElement(child, {
goToNextFunction: () => {
setActiveIndex(activeIndex + 1);
},
index,
isActive: index === activeIndex,
});
});
return <div>{childrenWithProps}</div>;
};
This works but feels a bit hacky.
The same result can be obtained with a render props
! pattern.
This one is more complicated, so bear with me.
Let's start with creating a renderFunction
. This function will accept the variables that the parent wants to give its children as the arguments and it will be returning a JSX element with the children to whom the arguments from parents are passed. We call such arguments RenderProps
.
const render = (parentRenderProps: IParentRenderProps) => {
return (
<>
<Child parentRenderProps={parentRenderProps} index={0} />
<Child parentRenderProps={parentRenderProps} index={1} />
<Child parentRenderProps={parentRenderProps} index={2} />
</>
)
};
Cool, the kids have the props we want them to have! Shall we go now? No. Cause we need to make use of this function.
In order to pass arguments to the render
method, we need to call it in the only place where the arguments are known - the parent!
So the parent will get this render
method in props:
<Parent className="test-name2" render={render} />
And then call it with the relevant arguments:
interface IParentProps {
render(parentRenderProps: IparentRenderProps): ReactNode; // it returns ReactNode - the JSX <> </>
}
const Parent: FunctionComponent<IParentProps> = ({
render,
}) => {
const [activeIndex, setActiveIndex] = useState<number>(0);
const showContent = (index: number) => setActiveIndex(index);
return (
<div className={className}>{render({ activeIndex, showContent })}</div>
);
};
export default QuoteAccordion;
The parent passed to its children activeIndex
and showContent
, now it's the childrens' turn to make use of them:
const Child: FunctionComponent<
IChild
> = ({ parentRenderProps, index }) => {
const showContent = (i: number) => parentRenderProps.showContent(i);
const { activeIndex } = parentRenderProps;
return (
<div>
<ChildHeader
index={index}
showContent={showContent}
title={`Child ${index} Header`}
/>
<ChildContent index={index} activeIndex={activeIndex}>
<StyledSampleContent
activeIndex={activeIndex}
showContent={showContent}
index={index}
/>
</ChildContent>
</div>
);
};
Voila!
Redux saga is a middleware for Redux that handles asynchronous actions. This is a boilerplate code to create saga, connect it with the store and run it.
import { applyMiddleware, compose, createStore } from 'redux';
import createSagaMiddleware from 'redux-saga';
...
import rootSaga from './Quotes/sagas';
const sagaMiddleware = createSagaMiddleware();
const enhancers = [applyMiddleware(sagaMiddleware), devToolsEnhancer({})];
const store = createStore(rootReducer, compose(...enhancers));
sagaMiddleware.run(rootSaga);
export default store;
The example below is of aan application which: 1) saves a quote to the backend, 2) once the quote is saved, takes its id and sets current_quote_id
in redux to this id. In other words it makes an async action and then dispatches action to updaate the store. This is exatly what saga is made for.
First I'll show you a function that in this example will be shaared by both saagaa and non-saga code. This function takes quote
object, saaves it in backend and returns the response.
saveQuote = async (quote: any) => {
const body: IQuoteBody = {
data: {
attributes: {
bandwidth: quote.bandwidth,
bearer: quote.bearer,
contract_term: quote.contract_term,
},
type: 'quote',
},
};
try {
const response = await doRequest({
body,
method: Methods.POST,
path: `/quotes`,
});
return response;
} catch (error) {
// console.log('we have error', error);
}
};
I want to save the quote on a click of button. In order to do it, in non-saga solution, I've imported the saveQuote
method and called it onClick, also passed store
I've received from mapStateToProps
.
...
import { saveQuote } from '../saveQuote';
interface IConnectionCapacity {
*quote: any;*
}
const ConnectionCapacity: FunctionComponent<IConnectionCapacity> = ({
quote,
}) => {
return (
...
<Button
*onClick={() => saveQuote(quote)}*
>
Save and continue
</Button>
);
};
const mapStateToProps = (state: any) => {
return {
*quote: state.quotes,*
};
};
export default connect(
mapStateToProps,
)(ConnectionCapacity);
As you remember, I noot only wanted to save a quote, but also save an id from response
as current_quote_id
in redux. The only way to do it without saga would be to dispatch an action from saveQuote directly on the store.
Let's start from creating this action:
export const setCurrentQuoteIdAction = (
current_quote_id: string
): ISetCurrentQuoteId => ({
payload: { current_quote_id },
type: QuoteActionTypes.SET_CURRENT_QUOTE_ID,
});
I'd also add this action to reducer:
case QuoteActionTypes.SET_CURRENT_QUOTE_ID:
draft.current_quote_id = action.payload.current_quote_id;
break;
Having this action would allow me to dispatch it directly on store in sveQuote
. I'd have to oimport store and then call store.dispatch(setCurrentQuoteIdAction(new_quote_id))
.
Instead of having to import store and dispaatch an action diirectly on it, we could use saaga middleware to deal with it.
This is my saga.tsx file, where I've created a createNewQuote generator function (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*) that calles the familiar saveQuote
function described below, and once it's done, it dispaatches action, the same one we created above, to change current_quote_id
in a store.
import { call, put, takeLatest } from 'redux-saga/effects';
import { setCurrentQuoteIdAction } from './actions';
import quoteOperations from '../Request/quoteOperations';
import { saveQuote } from './types/actions';
export function* createNewQuote({ payload }: { payload: any }) {
const newQuote = yield call(saveQuote, payload.quote); // saveQuote
yield put(setCurrentQuoteIdAction(newQuote.data.id)); // set current_quote_id
}
The next step is to tell saga when do we want createNewQuote
to be called. This is where saga watchers comes into play. In this case we're using takeLatest
, which activates the saga each time an action is dispatched to the store. But we're not talking here about the setCurrentQuoteIdAction
- we need another action for crating a quote.
export const createQuoteAction = (quote: any): ICreateQuote => ({
payload: { quote },
type: QuoteActionTypes.CREATE_QUOTE,
});
And this is the watcher - ech time CREATE_QUOTE
is dispatched, createNewQuote
will be clled.
export default function* rootSaga() {
yield takeLatest(QuoteActionTypes.CREATE_QUOTE, createNewQuote);
}
// note: we don't have to add naything to reducer as this aaction doesn't make changes to the store.
Now all we have to do is dispatch the CREATE_QUOTE
action from out component. In order to get access to this action, our component needsmapDispatchToProps
.
...
import { createQuoteAction } from '../actions';
interface IConnectionCapacity {
quote: any;
*createQuote(quote: any): void;*
}
const ConnectionCapacity: FunctionComponent<IConnectionCapacity> = ({
quote,
createQuote,
}) => {
return (
...
<Button
id="connectionCapacity_saveAndContinueButton"
mainStyle="secondaryRectangular"
*onClick={() => createQuote(quote)}*
>
Save and continue
</Button>
);
};
const mapDispatchToProps = (dispatch: any) => {
return {
*createQuote: (quote: any) => dispatch(createQuoteAction(quote)),*
};
};
const mapStateToProps = (state: any) => {
return {
quote: state.quotes,
};
};
export default connect(
mapStateToProps,
mapDispatchToProps
)(ConnectionCapacity);
Voila!
In some situations, we might want to know the previous state. For example, we're making a fetch that changes inProgress
field to true. Once it's over, the field will be changd to false. We'd like to show certain message only in situations when the state was first true and then false. We can do it by using a combination of useRef
and useEffect
hooks.
We have a value from the store - saving
. It turns into true when saving process gets activated. We can grab this initial value and store it in current using useRef()
.
const prevSaving = usePreviousState(saving);
export function usePreviousState(state: any) {
const ref = useRef();
useEffect(() => {
ref.current = state;
});
return ref.current;
}
Then when the saving status changes (once change is ready), we will have both - the old status and the new one.
useEffect(() => {
if (prevSaving && !saving && !savingError) {
accordion.showContent(accordion.index + 1);
}
}, [accordion, prevSaving, saving, savingError]);
https://medium.com/@qjli/how-to-mock-specific-module-function-in-jest-715e39a391f4