Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Solution] Displaying Mentions Away from TextInput #75

Closed
swushi opened this issue Nov 17, 2021 · 10 comments
Closed

[Solution] Displaying Mentions Away from TextInput #75

swushi opened this issue Nov 17, 2021 · 10 comments
Labels
enhancement New feature or request v3 Feature and fixes for the major v3 release
Projects

Comments

@swushi
Copy link

swushi commented Nov 17, 2021

Problem

This library has one key pitfall, it has is the inability to render the suggestions component apart from the TextInput. This is the root of some difficult styling challenges. I can see from a number of issues on your repo that this is popular issue. I am writing my solution out as there has not been a great solution I've seen.

Solution

Elevate the mention state up to a provider.

This would essentially look like this:

Parent Screen

<SuggestionsProvider>
  <Screen>
    <MentionInput />
    <Suggestions />
  </Screen>
</SuggestionsProvider>

Suggestions Component

const { suggestions } = useSuggestions();
{suggestions.map(item => <Suggestion {...item} />) }

Part Type Creator

You basically need to allow the suggestions provider to take in config for each respective trigger. My implementation for that looks like this:

export default function usePartTypeCreator() {
  const { setConfig, config } = useSuggestions();
  
  const createPartTypes = useCallback<
    (triggers: SupportedSuggestionTrigger[]) => PartType[]
  >(
    triggers =>
	triggers.map(trigger => ({
          trigger,
          textStyle: { fontWeight: 'bold' }, 
          allowedSpacesCount: 0,
          isInsertSpaceAfterMention: true,
          
          // Note: This is not directly rendering suggestions. It is just passing up null for render.
          // We are passing the data up to the provider which can then be consumbed from anywhere.
          renderSuggestions: props => {
	      if (config?.[trigger]?.keyword !== props.keyword) {
		setConfig(oldConfig => ({ ...oldConfig, [trigger]: props }));
	      }
	      return null;
          }
	})),
  [config]
);

	return createPartTypes;
}

Usage for that looks like:

const createPartTypes = usePartTypeCreator();
<MentionInput partTypes={createPartTypes('#', '@')} />

SuggestionsProvider Interface

This is how I've exposed the provider for usage throughout the app.

export default function useSuggestions() {
  const suggestions = useContext(SuggestionContext);
  
  const mentions = suggestions.config?.['@'];
  const topics = suggestions.config?.['#'];
  
  return { ...suggestions, mentions, topics };
}

If anyone else decides to go down this rabbit hole, let me know. I have a working implementation in our production app, and would be happy to help.

@HHEntertainment
Copy link

@swushi @dabakovich I think it is awesome suggetions!

@grooveslap
Copy link

grooveslap commented Dec 19, 2021

@swushi Thank you for your great solution. I am having some little difficulties implementing as you suggested above. So could you please share your working implementation in your production. Thank you in advance!

@swushi
Copy link
Author

swushi commented Dec 20, 2021

This is my working provider in our production app, @grooveslap

import React, { createContext, useCallback, useState } from 'react';

import { useDebugLog } from 'hooks/debug/useDebugLog';
import { useRenderCounter } from 'hooks/debug/useRenderCounter';

import { SuggestionConfig } from 'hooks/providers/usePartTypeCreator';

const debug = false;
const debugLogs = false && debug;
const debugRenders = false && debug;
interface SuggestionContextConfig {
	config?: SuggestionConfig;
	setConfig: React.Dispatch<React.SetStateAction<SuggestionConfig | undefined>>;
	clearConfig: () => void;
}

export const SuggestionContext = createContext<SuggestionContextConfig>({} as SuggestionContextConfig);

/**
 * A provider that should wrap the screen that contains a `MentionInput`
 *
 * The purpose of this provider is to allow the suggestions component to
 * be rendered outside of the input. Currently it is forced to be attached directly to the
 * input, which is usually in a inconvenient location.
 *
 * @example
 * const suggestions = useSuggestions()
 *
 * <SuggestionsProvider>
 *   <Screen>
 *     <MentionInput />
 *
 *     // can separate suggestions now, was previously attached to the above component
 *     {suggestions && renderSuggestions()}
 *
 *   </Screen>
 * </SuggestionsProvider>
 */
const SuggestionsProvider: React.FC = ({ children }) => {
	useRenderCounter('SuggestionsProvider', debugRenders);
	const debugLog = useDebugLog('SuggestionsProvider', debugLogs);

	const [config, setConfig] = useState<SuggestionConfig>();

	const clearConfig = useCallback(() => {
		setConfig(undefined);
	}, []);

	return (
		<SuggestionContext.Provider
			value={{
				config,
				setConfig,
				clearConfig
			}}
		>
			{children}
		</SuggestionContext.Provider>
	);
};

export default SuggestionsProvider;

And here is the useSuggestions hook:

import { useContext, useEffect, useMemo, useState } from 'react';
import { SuggestionContext } from 'providers/SuggestionsProvider/SuggestionsProvider';
import { maybe } from 'typescript-monads';

export default function useSuggestions() {
	const suggestions = useContext(SuggestionContext);

	const mentions = useMemo(() => suggestions.config?.['@'], [suggestions.config]);
	const topics = useMemo(() => suggestions.config?.['#'], [suggestions.config]);

	/**
	 * calm keyword is needed as react-native-controlled-mentions fires off random undefined values.
	 * see {@link UsernameSuggestionsMemo}
	 */
	const [calmKeyword, setCalmKeyword] = useState<string>();

	useEffect(() => {
		const timeout = setTimeout(() => setCalmKeyword(mentions?.keyword ?? topics?.keyword), 10);

		return () => clearTimeout(timeout);
	}, [mentions?.keyword, topics?.keyword]);

	const isSuggesting = useMemo(() => {
		return maybe(calmKeyword).isSome();
	}, [calmKeyword]);

	return { ...suggestions, mentions, topics, isSuggesting };
}

@grooveslap
Copy link

grooveslap commented Dec 21, 2021

@swushi Thank you so much! You're the man!

@aloukissas
Copy link

aloukissas commented Dec 23, 2021

@swushi this is great! Can you perhaps share the type definition for SuggestionConfig? I think I have it figured out, but it would help others. I ended up making a similar solution using jotai instead of a provider, but the main gist that should help people here is: (a) use the renderSuggestions function as a conduit to set the state of the incoming MentionSuggestionsProps; (b) consume and use this state with keyword and onSuggestionPress in a sibling react node to the MentionInput that you control fully.

@swushi
Copy link
Author

swushi commented Dec 27, 2021

@aloukissas here is my type for SuggestionConfig

type SupportedSuggestionTrigger = '@' | '#';

type SuggestionConfig = {
  [Property in SupportedSuggestionTrigger]?: MentionSuggestionsProps | undefined;
};

This mapped type essentially creates this type:

type SuggestionConfig = {
  '@'?: MentionSuggestionsProps;
  '#'?: MentionSuggestionsProps;
};

MentionSuggestionsProps is the type exported by this library.

I am not familiar with jotai myself, but after reading through their docs it looks like a great library. Is it not using providers under the hood?

@aloukissas
Copy link

Awesome thanks @swushi. I believe it's using react context under the hood. It's really a lightweight implementation of Facebook's Recoil. Very happy with it.

dabakovich added a commit that referenced this issue Apr 26, 2022
Add `onMentionsChange` property to the `MentionInput` component. It allows to render suggestions outside `MentionInput` component — #65, #69, #75.

Fix double trigger of suggestions render. Now we are trigger this only after selection change — #55.

Format `utils.ts` using prettier.

BREAKING CHANGES

Remove `containerStyle` prop from the `MentionInput` component.

Remove `inputRef` prop from the `MentionInput` component. Use traditional `ref` now.

Remove `renderSuggestions` and `isBottomMentionSuggestionsRender` from the Part type.

Rename `MentionSuggestionsProps` type to `SuggestionsProvidedProps`. Rename `onSuggestionPress` to `onSelect` in the type.
@dabakovich
Copy link
Owner

Hi @swushi and other participants! Thank all for your feedbacks and especially for participating all in searching for solution of this issue.

Finally, I got a few free days for supporting this package.

Your suggestion with using context inspired me to re-think the functionality with rendering suggestions. In fact, we have no need to render it in the MentionInput. And more – we don't need the MentionInput component as well. It's enough to provide just a few props directly to the TextInput component.

So, I'd be glad to say that a new major v3 release is coming, and this issue is one of the main updates 🎉.

Now you will be able to use hook for control mention state. The main idea – suggestions rendering now will be fully controlled by user and is extracted from MentionInput component.

You can find an example of this here: https://github.com/dabakovich/react-native-controlled-mentions/blob/3.0.0-feat-use-mentions/example/mentions-app.tsx#L90

To install this and play by yourself, please use next commands:

yarn add react-native-controlled-mentions@alpha
// or
npm install --save react-native-controlled-mentions@alpha

I'll be happy to get some feedback from you before the major update will be released.

@dabakovich dabakovich added enhancement New feature or request v3 Feature and fixes for the major v3 release labels May 1, 2022
@hirbod
Copy link

hirbod commented May 12, 2022

@dabakovich these are great news. Gonna try that very soon!

@dabakovich dabakovich added this to In progress in V3 May 14, 2022
@dabakovich dabakovich moved this from In progress to Ready In Alpha in V3 May 14, 2022
@dabakovich
Copy link
Owner

I'm closing the issue due to the lack of new questions and activity. You are welcome to add comments or create a new issue if you'll get new questions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request v3 Feature and fixes for the major v3 release
Projects
V3
Ready In Alpha
Development

No branches or pull requests

6 participants