WaitForPath: A Solution To Endless WaitForChild Calls

If you’re like me, your interfaces are both a) not built with Roact and b) have a whole lot of nested components. As a result, spamming UI scripts with WaitForChild calls seems almost inevitable.

Last year I wound up making a function to deal with the never-ending pain, and I’ve had a fair amount of success with it so I think it’s worth sharing here. It works by accepting the object to start searching from, and then an object path as a string, the same way you would write it to normally reference an object. It also accepts an optional maxWait parameter. Here’s an example(s?):

Example

local WaitForPath=require(game.ReplicatedStorage.WaitForPath)--assuming this is the place the module is stored
local player=game.Players.LocalPlayer
local interface=WaitForPath(player,"PlayerGui.Interface")
local superNestedCloseButton=WaitForPath(interface,"MainMenu.Panels.SuperCoolPanel.Options.Close") --returns the button
local otherButtonButWithMaxWait=WaitForPath(interface,"SecondMenu.Panels.SuperCoolerPanel.Options.MaybeClose",5)--returns button, or nil after 5 seconds

Usage

Instance WaitForPath(Instance target,string path[,number maxWait])

Source

Here’s a .rbxmx file you can drop into your game:
WaitForPath.rbxmx (3.2 KB)

And here’s the source if you’d rather just copy-paste it into a ModuleScript:

local BAD_ARG_ERROR="%s is not a valid %s"

local function WaitForPath(target,path,maxWait)
	do --error checking
		local tt=typeof(target)
		local tp=typeof(path)
		local tm=typeof(maxWait)
		if tt~="Instance" then error(BAD_ARG_ERROR:format("Argument 1","Instance")) end
		if tp~="string" then error(BAD_ARG_ERROR:format("Argument 2","string")) end
		if tm~="nil" and tm~="number" then error(BAD_ARG_ERROR:format("Argument 3","number")) end
	end
	local segments=string.split(path,".")
	local latest
	local start=tick()
	for index,segment in pairs(segments) do
		if maxWait then
			latest=target:WaitForChild(segment,(start+maxWait)-tick())
		else
			latest=target:WaitForChild(segment)
		end
		if latest then
			target=latest
		else
			return nil
		end
	end
	return latest
end

return WaitForPath

Bits

  • If the path can’t be found within the allotted maxTime, the function will return nil.
  • If the path doesn’t exist and no maxTime has been set, the function will yield forever.
  • I’ve poked and prodded this function a few different ways since I made it, so although it works for me, depending on how you use it you might run into some janky behavior. Chipio IndustriesTM is not liable for any jankTM you encounter while using this function.
  • Do not read the .rbxmx
78 Likes

This is so awesome! Thanks for the great resources!

2 Likes

Honestly I should have written something like this years ago. Thanks for the free WaitForPath! I’ve probably come close to the local variable limit a few times with WaitForChild’s in a highly nested interface. :confused:

Honestly, that did bother me sooo much; especially when dealing with UI stuff. Just having a hundred :WaitForChild() was so obnoxious and tedious. Yet I never bothered to try making something like this lol

Great job!


Also a little question:
Would you mind if I included this function into this script command module that I’ve been working on for a while? With credit to you, of course. It’s meant to make certain tasks less annoying while scripting, and I think this would be a perfect fit for it (if you’ll allow it).

Like ik it’s out here for everyone to use, but to include it on another module for stuff seems kinda weird and felt it was appropriate to ask first.

This is really great! Had a few problems with WaitForChild() so, this would come in handy.

Totally, go for it. When you break down the script, it’s pretty simple anyway.

2 Likes

Ok this is a pretty smart idea, I never thought of it. Great job!

1 Like

Just curious, why did you do that for error checking instead of just using the assert() function?

I wasn’t too familiar with assert() when I wrote this. You were warned of the jank. :stuck_out_tongue:

1 Like

Hi @ChipioIndustries, thanks for sharing us your resource!

I have one concern:

  • how does this accommodate for Instances with “.” in their name?

(I acknowledge that it’s a very rare use case)

It doesn’t, unfortunately. Your best bet is to use a separate WaitForChild() call for any particular instances with a period in the name.

I see, I just thought of something for this specific problem:

As there will always be an additional “dot” for indexing you could extract that.

For example

  • .Name
  • Name.
  • .Name.

You’d detect when there are is a additional “dot” because there’s always a “dot” added to index the instance, then replace that “dot” with a Character nobody would use and then split it with that

&$#^~*

1 Like

Thank you so much for this! This helped me out so much!

1 Like

Hey :wave: Good work!

Here’s a slightly revised version:

local function WaitForPath(Ancestor: Instance, path: string, timeout: number?): Instance?
	local startTime = tick()
	for _, name in ipairs(string.split(path, '.')) do
		local Child = Ancestor and Ancestor:WaitForChild(name, timeout and math.max(0.001, startTime - tick() + timeout))
		Ancestor = Child
	end
	return Ancestor
end

ipairs is for safety, because pairs isn’t guaranteed to loop in order (last I checked).

math.max is also for safety - it would be an extremely rare case, but if there was close to 0 seconds left in the “wait budget”, the next timeout value could end up being <= 0 causing WaitForChild to error.

The rest is just readability, or, I should say compactness…ability… reusing variables, using inline logic, etc.

I did test performance (excluding the type-checks), and my version is 2% slower when no timeout is used, and 15% faster when a timeout is used. I have no idea why it’s slower in the first case, I tested different things. But in either case these differences are negligible.

What’s even weirder is that I initially had this on line 5:

local Child = Ancestor and (Ancestor:FindFirstChild(name) or Ancestor:WaitForChild(name, timeout and math.max(0.001, startTime - tick() + timeout)))

and that made it 5% slower. I remember being taught that WaitForChild may yield for a frame when the child Instance is already present, and thus FindFirstChild(x) or WaitForChild(x) should be used. The only explanation I have is that that’s false and that WaitForChild probably already has such a logic internally.