Skip to content

Instantly share code, notes, and snippets.

@Kotauror
Last active May 18, 2020 10:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Kotauror/bdd470d0e88e27960dbc55812eab5a11 to your computer and use it in GitHub Desktop.
Save Kotauror/bdd470d0e88e27960dbc55812eab5a11 to your computer and use it in GitHub Desktop.
JavaScript the harder parts.

React / TS - thinks that I've recentry learned / refreshed

Table of Contents

Easy Section


Harder Section

Passing props to unknown children - clone and render props.

Easy Section

Calling an onClick method with an argument

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>

Object property value shorthand

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.

Add logic to an arrow function with implicit return

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,
  })
};

Add logic to a Function Component with implicit return

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}
        />
       ...
    );

Object deconstruction

Example 1

var { cat } = {cat: "kocilla"}
console.log(cat)
// kocilla

Example 2

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,
});

Example 3

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

Example 4

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());
  }
}

Testing connected Redux component

When you haave a connected 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>
        );

     ...

Spread operator

Update various fields in an object with the same piece of logic (ex. 1)

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
      },
    },
  });

Update various fields in an object with the same piece of logic (ex.2)

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));

Keyof in TypeScript

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 

Non null operator

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 in async annonymous function

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);
      }
    })();
  }
}, []);

Checking location in tests

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'));
  });

File saver package

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: {
       ...
      },
    });
  });
});

Harder Section

Passing props to components in Route - using function returning a function

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 components.

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 unknown children - clone and render props pattern

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.

with React.cloneElement

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.

With 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

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.

Adding saga

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!

using useEffect with useRef to save previous state

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]);

Mock function in module

https://medium.com/@qjli/how-to-mock-specific-module-function-in-jest-715e39a391f4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment